背景故事 在遥远的魔法王国里,拓拓和思思是两位年轻的魔法学徒。他们发现了一串神秘的魔法宝石,每颗宝石都蕴含着不同的魔力值A1,A2,…,AN。为了提升自己的魔法能力,他们决定进行一项挑战: 规则 :每次选择魔力值最小的宝石(如果有多颗魔力值相同,就选最左边的那颗),将它从宝石序列中移除。然后,这颗宝石的魔力会传递给相邻的宝石,使它们的魔力值增加被移除宝石的魔力值。 问题描述 给定一串魔法宝石,初始魔力值为A1,A2,…,AN。拓拓和思思需要执行以下操作K次: 1.选择宝石 :找到当前序列中魔力值最小的宝石。如果有多颗魔力值相同,选择最左边的那颗。 2.移除宝石并传递魔力 : 移除选中的宝石。 如果这颗宝石左边有相邻宝石 ,则左边宝石的魔力值增加被移除宝石的魔力值。 如果这颗宝石右边有相邻宝石 ,则右边宝石的魔力值增加被移除宝石的魔力值。 最终,输出K次操作后剩下的宝石序列。 输入格式 第一行包含两个整数N和K。 第二行包含N个整数,A1,A2,A3,…,AN。 输出格式 输出N-K个整数,中间用一个空格隔开,代表K次操作后的宝石序列。 样例 输入数据#1 5 3 1 4 2 8 7 输出数据#1 17 7 解释#1 数列变化如下,中括号里的数是当次操作中被选择的数: [1] 4 2 8 7 5 [2] 8 7 [7] 10 7 17 7 数据范围 对于20%的数据,1≤K<N≤10000。 对于100%的数据,1≤K<N≤10^5,0≤Ai≤10^8。 c++

时间: 2025-03-30 19:09:23 浏览: 64
<think>好的,我现在得仔细看看这个问题。题目是说,每次移除当前序列中魔力值最小的最左边的宝石,然后它的左右相邻的宝石会加上这个被移除的宝石的魔力值。然后重复这个过程K次,最后输出剩下的序列。 那怎么模拟这个过程呢?直接模拟的话,每次都要找到最小值的位置,然后处理左右,然后更新数组。但问题在于当N和K很大的时候,比如1e5次操作的话,这样的时间复杂度会很高。每次找最小值是O(n),K次的话总时间复杂度是O(Kn),这样对于1e5来说,1e5 * 1e5就是1e10次操作,肯定超时了。所以必须得想更高效的方法。 那怎么优化呢?想到可能需要用优先队列或者堆结构来快速找到最小值。但是,每次移除后,左右元素的数值会变化,这会影响到后面的操作。这时候,如何维护这些元素的当前值呢?因为当某个元素被移除后,周围的元素的值会被更新,而这些被更新的元素可能之后会被选中。所以,直接使用堆的话,如果元素的值被修改了,堆中的旧值可能还存在,导致无法正确找到最小值。这个时候,可能需要一个可以动态更新元素的结构,比如一个索引优先队列,或者某种能够支持高效更新和删除的数据结构。 或者,有没有可能使用双向链表来维护宝石的顺序,并且每个节点保存当前的值,以及左右指针。然后,用一个优先队列或者堆来保存每个节点的当前值,以及位置信息。但问题在于,当某个节点被移除后,会影响左右节点的值,这时候这些左右节点的值需要被更新,而堆里的旧值需要被移除或者标记为无效。但普通的堆结构无法处理这种情况,因为堆里的元素无法被动态修改。 这时候想到,可能可以采用一种延迟处理的策略。比如,维护一个优先队列,里面保存每个元素的当前值以及位置,但当一个元素被移除后,检查堆顶的元素是否有效(即是否已经被处理过,或者其保存的值是否已经被更新)。只有当堆顶的元素的值确实是当前的有效值时,才进行处理。否则,就忽略这个堆顶元素,继续弹出下一个元素。 具体来说,每个元素可能需要记录其当前的魔力值,以及是否已经被移除。当处理一个元素时,首先检查它是否已经被移除或者它的值是否已经被修改过。如果是的话,就跳过,否则进行移除操作,并更新左右邻居的值,然后将左右邻居的新值重新加入堆中,或者标记原有堆中的记录为无效。 这可能需要每个节点有一个指针或者索引,指向其在堆中的位置,以便在更新值时能够快速调整堆的结构。但这样实现起来可能比较复杂,尤其是在C++中,因为标准库的优先队列不支持这样的操作。 或者,可以考虑使用一个优先队列,存储的是可能的候选节点,但每次取出堆顶的时候,需要检查该节点是否已经被处理过,或者它的值是否已经过时。如果是的话,就跳过,继续处理下一个堆顶元素。否则,处理该节点,并更新其左右节点的值,同时将这些左右节点的新值加入堆中。这样,堆中可能有很多旧的记录,但每次处理的时候都会先进行有效性检查。 这种方法的时间复杂度如何?每次操作可能需要多次弹出堆顶元素,直到找到一个有效的。假设每个元素被多次加入到堆中,但每次处理的时候会处理掉一次。如果K次操作的话,总的堆操作次数可能接近O(K log N)。这可能可行。 那具体来说,如何实现呢? 首先,每个节点需要维护其当前的值,是否被移除,以及左右邻居的指针。可以用双向链表的结构来维护宝石的顺序。同时,用一个优先队列(最小堆)来保存每个节点的当前值和位置。但是,堆中的元素可能包含已经被处理过的节点,或者已经被更新过的节点的旧值。所以,每次从堆顶取出元素时,需要检查该节点是否已经被移除,或者该节点的当前值是否等于堆中保存的值。如果不等,说明这个堆中的记录是过时的,应该跳过。 具体步骤: 1. 初始化双向链表,每个节点保存其值、左右指针、是否被移除的标记。 2. 将所有节点的初始值以及它们的索引(或者指针)放入一个优先队列中。优先队列的比较依据是节点的值,以及当值相同时选择左边的。 3. 进行K次操作: a. 从优先队列中取出堆顶元素(最小值,最左边)。 b. 检查该节点是否已经被移除,或者其当前值是否等于堆中的值。如果是无效的,继续弹出堆顶直到找到有效的节点。 c. 找到该节点的左邻居和右邻居。 d. 将该节点标记为已移除,并从链表中删除。例如,左邻居的右指针指向右邻居,右邻居的左指针指向左邻居。 e. 将该节点的值加到左邻居的值中(如果存在左邻居),同样加到右邻居的值中(如果存在右邻居)。 f. 将左邻居和右邻居的新值加入到优先队列中。因为它们的值已经被更新,旧的堆记录可能还在堆中,但会被后续的检查跳过。 4. 最后遍历链表,输出剩余节点的值。 需要注意的是,每个节点可能有多个记录在堆中,对应不同时间的值。但只有最新的值才会在堆中被正确使用。旧的记录会被检查到无效后跳过。 这样,每次操作的时间复杂度主要取决于堆的操作次数。假设每次处理一个节点需要弹出若干无效的堆元素,但每个节点的无效记录最多被弹出一次,那么总的时间复杂度可能为O((N + K) log N),这在K和N都是1e5的情况下是可行的。 接下来要考虑如何实现双向链表以及节点的管理。可以使用结构体或者类来表示每个节点。例如: struct Node { int value; Node* left; Node* right; bool removed; }; 但动态分配可能比较麻烦,或者在大量数据时效率不够。或者,可以用数组来模拟链表。比如,使用两个数组prev和next来保存每个节点的前驱和后继,再用一个数组values保存当前的值,以及一个removed数组标记是否被移除。这样,对于索引i来说,prev[i]是其前驱,next[i]是其后继,values[i]是当前的值。 初始化时,prev[i] = i-1,next[i] = i+1。注意边界条件,比如i=0时prev是-1,i=N-1时next是-1。 然后,优先队列中存储的元素需要包括节点的当前值以及它的索引。但是,当节点的值被更新后,堆中的旧值不会被自动更新。因此,每次从堆中取出一个元素时,需要比较该元素的索引对应的当前值是否等于堆中保存的值。如果不等于,或者该节点已经被移除,则跳过。 例如,堆中的元素是pair<int, int>类型,其中第一个元素是value,第二个是索引。但为了处理相同值时选择最左边的,可能需要自定义比较函数。或者,堆中的元素可以保存-value和索引,这样用最大堆就能得到最小值。或者,使用优先队列的比较函数,使得当两个元素的值相等时,索引较小的排在前面。 C++的优先队列默认是最大堆,所以我们可以用pair的结构,其中第一个元素是value,第二个是index,并且自定义比较函数,使得当value较小时排在前面,当value相等时,index较小的排在前面。比如: struct Compare { bool operator()(const pair<int, int>& a, const pair<int, int>& b) { if (a.first != b.first) { return a.first > b.first; // 小顶堆,因为优先队列默认是最大的排在前面,所以这里返回true时,a的优先级低,排在后面。 } else { return a.second > b.second; // 当value相等时,索引大的排在后面,所以堆顶是索引较小的。 } } }; priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> pq; 这样,每次弹出的堆顶元素就是当前最小的value,且相同value时最左边的。 然后,每次弹出元素后,检查该元素的索引对应的当前value是否等于堆中的value,并且是否未被移除。如果都满足,则处理该节点。否则,跳过。 处理步骤: 对于弹出的元素(value, index): 检查: if (removed[index] || values[index] != value) → 无效,跳过。 否则: 将该节点标记为removed[index] = true. 然后,找到其左邻居和右邻居: left = prev[index]; right = next[index]; 如果左存在,那么values[left] += value. 如果右存在,那么values[right] += value. 然后,将左和右的新值插入堆中。 之后,更新链表结构: 如果左存在,则next[left] = right. 如果 right存在,则 prev[right] = left. 这样,链表的结构被维护。 这样,每次处理一个有效节点时,会将其左右邻居的值增加,并将左右邻居的新值入堆。而之前的旧值可能还在堆中,但在之后处理时会被检查到无效(因为它们的values数组已经被更新)。 这样,整个过程可以高效地进行。 现在,测试样例来看: 输入样例: 5 3 1 4 2 8 7 初始化后的各个节点的索引是0到4,对应的值分别是1,4,2,8,7. 初始链表: prev数组:[-1,0,1,2,3] next数组:[1,2,3,4,-1] 初始堆中有这5个元素: (1,0), (4,1), (2,2), (8,3), (7,4) 第一次操作: 取出堆顶元素(1,0)。检查未被移除,且values[0]等于1。处理: 标记removed[0] = true. 左邻居是prev[0] = -1 → 不存在。右邻居是next[0] =1. 所以,右邻居(索引1)的值增加1 → 4+1=5。于是,values[1]变为5。 将(5,1)加入堆。 然后,更新链表:原0的左右是左不存在,右是1。所以,将next[left](不存在)指向right,prev[right](索引1)的prev设为-1。 所以,现在的链表结构是: 索引1的prev是-1,next是2。其他节点的关系: prev数组变为:[-1, -1, 1, 2,3] next数组变为:[1,2,3,4,-1] → 但原0已经被移除,索引1的prev变为-1,所以索引1的prev是-1,索引0被移除。 此时,剩下的节点是1(索引1)、2(索引2)、3(索引3)、4(索引4)吗?或者原链表中的节点是索引0被移除,剩下的四个? 不管怎样,第一次处理后,序列变为5,2,8,7?或者原序列中的索引0被移除,索引1的值变为5,所以序列是5,2,8,7? 然后,第二次操作: 堆中现在包含的元素有(4,1)(旧值)、(2,2)、(5,1)(新插入的),8,3,7,4,以及可能其他元素? 但处理的时候,当取出堆顶元素时,会先取出(2,2)吗? 因为原堆中还有旧的元素,比如(4,1)已经被更新为5,所以当堆中有(4,1)和(5,1)的时候,此时堆中的元素可能被弹出顺序如何? 比如,假设此时堆中有(4,1)和(5,1)两个元素。当取出(2,2)时,是正确的,因为此时该节点的当前值是否为2?是的,未被处理过。所以第二次操作选中索引2,值为2。 处理: removed[2] = true. 左邻居是prev[2] =1(索引1),右邻居是next[2]=3(索引3)。 左的值变为5+2=7,右的值变为8+2=10. 然后将这两个新值(7,1)和(10,3)插入堆中。 更新链表:索引2被移除,索引1的next指向3,索引3的prev指向1. 第三次操作,需要处理第三个最小值。此时剩下的节点是索引1(7)、3(10)、4(7)吗? 此时,堆中可能有很多元素。比如,堆中可能有(5,1)(此时该节点的值已经是7了?)或者(7,1)被插入。可能这时候堆中的元素包括(5,1)、(2,2)、(7,1)、(10,3)、(7,4)等等。当弹出堆顶元素时,会找到当前的最小值。 第三次处理时,堆顶可能是7,但有两个节点:索引4的值是7吗? 比如,第三次操作时,剩下的节点是索引1(7)、3(10)、4(7)吗? 此时,堆中的元素可能包括: 索引1的值是7,索引3的值是10,索引4的值是7。或者还有其他元素? 假设第三次处理的是索引4的值7,或者索引1的值7? 根据规则,当值相同时,选择最左边的。所以此时堆中的元素如果有两个7,比如索引1和索引4,那么应该选择索引较小的,即索引1。 但可能此时,索引1的当前值是7,而索引4的值是7。堆中有元素(7,1)和(7,4)。堆顶是(7,1)。 所以第三次处理的是索引1。这时候: 标记为已移除。然后左邻居不存在(因为prev[1]是-1),右邻居是3。所以,右邻居的值变为10+7=17。 然后,将新值17插入堆。 此时,剩下的节点是索引3和4吗? 或者,在第三次操作后,索引1被移除,剩下的节点是索引3和4? 或者可能还有之前的节点? 总之,经过三次操作后,剩下的两个节点是索引3和4,值分别为17和7。所以输出17 7,与样例一致。 这说明该算法是正确的。 现在,问题的关键是如何高效地实现这个结构,尤其是处理堆中的旧元素。 在C++中,优先队列不支持直接删除或更新元素,所以只能通过延迟处理的方式。也就是说,每次弹出堆顶元素时,检查其对应的值是否有效。如果无效,则跳过。这可能导致堆中存在大量的旧元素,但每个有效元素只会被处理一次。因此,总的时间复杂度应该还是可以接受的,因为每个元素最多被插入堆中O(1)次?或者每次修改都可能插入一次? 比如,每个节点在被左右邻居移除时,可能会被多次修改值,导致多次插入堆中。例如,某个节点的初始值是4,然后被更新到5,再被更新到7,每次更新都会有一个新的元素被插入堆中。因此,堆中的元素总数可能会是O(K + N)。假设每次处理一个有效节点,会产生两个新元素插入堆(左右邻居的更新),那么总共有O(K)次有效处理,每次产生两个元素,所以堆的总元素数是O(N + K)。每次堆操作是O(log (N+K)),总时间复杂度是O( (N + K) log (N + K) )。 对于N=1e5,K=1e5的情况,N+K=2e5,log是20左右,所以总操作次数是2e5 * 20 =4e6,这应该可以通过时间限制。 现在,如何实现呢? 具体的步骤: 1. 读取N和K,然后读取数组A。 2. 初始化prev和next数组,每个元素的prev是i-1,next是i+1。边界条件是prev[0]=-1,next[N-1] =-1。 3. 初始化values数组为输入的数组。 4. 初始化一个removed数组,所有元素初始化为false。 5. 将每个元素的(value, index)插入优先队列。 6. 进行K次有效操作: a. 循环:从优先队列中取出堆顶元素(value, index)。 b. 检查该元素是否已经被移除(removed[index]为true),或者当前values[index] != value。如果是,跳过这个元素,继续循环。 c. 否则,处理这个元素: i. removed[index] = true. ii. 找到左邻居left_idx = prev[index], 右邻居right_idx = next[index]. iii. 如果left_idx != -1: values[left_idx] += value. 将(values[left_idx], left_idx)插入堆中. iv. 同理,如果right_idx != -1: values[right_idx] += value. 将(values[right_idx], right_idx)插入堆中. v. 更新链表结构:将left_idx的next指向right_idx,将right_idx的prev指向left_idx. vi. 计数器减一,直到完成K次处理。 7. 处理完成后,遍历链表,收集剩下的元素。如何遍历链表呢?可以找到链表的头部,即找到第一个未被移除的元素,然后根据next指针遍历。或者,可以使用一个数组,逐个检查每个元素是否未被移除,然后按照链表顺序收集。 但是,链表的结构可能已经被修改,所以直接遍历每个未被移除的元素的顺序可能不正确。例如,假设原链表是0->1->2->3->4,其中0被移除,链表变成1->2->3->4。然后1的prev是-1,next是2。所以正确的顺序是1,2,3,4。所以,正确的遍历方法是找到头节点(即prev为-1的节点),然后根据next指针遍历。 所以,如何找到头节点? 可以初始化头节点为0,然后循环判断该节点是否被移除。如果被移除,则头节点变成next[head]。直到找到一个未被移除的节点。或者,这可能有问题。或者,可以维护头节点和尾节点,但可能比较复杂。 更简单的方法是,遍历所有节点,找到未被移除的节点,然后根据链表结构来遍历。例如,找到第一个未被移除的节点,然后根据next指针遍历,直到结束。 或者,在每次处理后,链表的结构已经被正确维护,所以可以通过prev和next数组来遍历剩下的节点。例如,头节点是某个未被移除的节点,其prev是-1。所以,可以找到所有未被移除的节点中,prev为-1的节点作为头节点,然后顺序遍历。 例如: int current = 0; while (current != -1) { if (!removed[current]) { // 这是头节点吗?可能不是,因为可能之前的节点被移除了。例如,原头节点是0,被移除后,头节点是1,其prev是-1? // 所以,正确的头节点是第一个未被移除的节点,其prev为-1. break; } current = next[current]; } 但这个方法可能无法正确找到头节点。例如,假设原链表的头节点是0,被移除后,头节点变成1,其prev被设置为-1。所以,在prev数组中,1的prev是-1。所以,正确的头节点是1。所以,正确的遍历方法是找到所有未被移除的节点中,prev为-1的那个节点作为头节点。 所以,可以遍历所有节点,找到未被移除的,且prev为-1的节点。这需要O(N)时间,但N可能很大,1e5,这会不会有问题? 或者,在每次移除节点时维护头节点和尾节点。例如,当移除头节点时,新的头节点是其next节点。这样,可以维护一个变量来记录当前链表的头节点,这样在最后遍历时不需要遍历所有节点。但这样会增加维护的复杂度。 或者,在最后遍历时,从prev数组中找到所有未被移除的节点,并记录它们的顺序。或者,可以采用以下方式: 遍历整个数组,收集未被移除的节点,然后根据prev和next数组的顺序重新构建顺序。 这似乎比较复杂。或者,可以采用以下方法: 使用一个数组result,然后找到链表的头节点(即prev为-1的未被移除的节点)。然后,依次访问next指针,直到到达尾节点。将每个未被移除的节点的值加入结果数组。 例如: int current = -1; // 找头节点:prev为-1的未被移除的节点 for (int i = 0; i < N; ++i) { if (!removed[i] && prev[i] == -1) { current = i; break; } } vector<int> res; while (current != -1) { res.push_back(values[current]); current = next[current]; } 输出res中的元素。 这应该可以得到正确的顺序。 综上,整个算法的步骤是可行的。现在需要考虑数据结构的选择。例如,prev和next数组可以用数组实现,每个元素是int类型,初始化为i-1和i+1。values数组保存当前每个节点的值。removed数组保存是否被移除。 现在,针对数据范围,N和K是1e5,需要处理大量的堆操作。所以在C++中,需要尽可能高效的实现。使用priority_queue,但要注意每次插入元素的时间复杂度。 现在,编写代码的大致步骤: 1. 读取输入N和K,以及数组A。 2. 初始化prev、next、values、removed数组。 3. 创建优先队列,并将所有节点的初始值入队。 4. 进行K次有效操作: while (K > 0) { auto top = pq.top(); pq.pop(); int val = top.first; int idx = top.second; if (removed[idx] || values[idx] != val) continue; // 处理这个节点 removed[idx] = true; K--; int left = prev[idx]; int right = next[idx]; // 更新左右邻居的值,并插入堆中 if (left != -1) { values[left] += val; pq.push({values[left], left}); next[left] = right; } if (right != -1) { values[right] += val; pq.push({values[right], right}); prev[right] = left; } } 5. 收集结果: 找到头节点,然后遍历链表。 但这里有一个问题,当处理一个节点时,如何更新prev和next数组? 例如,当移除idx时,其左邻居的next应该指向idx的右邻居,右邻居的prev应该指向左邻居。所以,在处理idx时,修改其左邻居的next和右邻居的prev。 例如: left = prev[idx]; right = next[idx]; 如果left != -1 → next[left] = right; 如果 right != -1 → prev[right] = left; 所以,在代码中,在更新左右邻居的prev和next时,应该这样做: if (left != -1) { next[left] = right; } if (right != -1) { prev[right] = left; } 但这段代码应该放在哪里?例如,当处理完该节点后,需要将左右邻居的指针连接起来。这应该在处理该节点时完成,无论是否更新左右的值。 哦,对。无论该节点的左右邻居是否被更新,他们的prev和next指针必须被正确修改,以维护链表的结构。所以,在代码中,当处理idx时: 处理步骤: removed[idx] = true; int left = prev[idx]; int right = next[idx]; // 更新左的next指向right if (left != -1) { next[left] = right; } // 更新右的prev指向left if (right != -1) { prev[right] = left; } // 然后,如果左存在,则其值增加val if (left != -1) { values[left] += val; pq.push({values[left], left}); } if (right != -1) { values[right] += val; pq.push({values[right], right}); } 这样,链表的结构被正确维护。例如,当移除idx后,左和右的指针被正确连接。 这样,在收集结果时,可以通过链表结构来遍历未被移除的节点。 现在,测试样例中的第一次操作: 移除索引0,左是-1,右是1。所以,左不存在。所以,next[left](不存在)无需处理。处理右的prev:将prev[1]设为-1? 是的。所以,prev[1] = left(left是-1)。所以,此时索引1的prev是-1,next是2。索引0被移除。 这样,链表结构正确。 第二次操作,处理索引2。其左是1,右是3。处理后的链表结构是,左的next(索引1的next)设为3。右的prev(索引3的prev)设为1。所以,索引2被移除,链表结构变成1->3->4。索引3的prev是1,索引4的prev是3,索引1的next是3。 第三次操作处理索引4吗?或者处理索引1?或者处理索引3? 第三次操作处理的是哪个节点? 第三次处理时,假设堆中有索引1的值为5+2=7(在第一次处理时,右邻居索引1的值被加1→4→5,然后在第二次处理索引2时,左邻居索引1的值被加2→5+2=7?或者可能我之前的例子中的数值变化是否正确? 例如,第一次操作: 索引0被移除,值是1。右邻居是1,所以索引1的值变为4+1=5。此时,堆中插入(5,1)。 第二次操作,处理索引2,初始值为2。左右邻居是1和3。索引1的值加2变为5+2=7,索引3的值加2变为8+2=10。所以,插入(7,1)和(10,3)。 此时,堆中有这两个元素。第三次操作时,堆顶的元素可能是什么? 假设第三次操作时,堆中的元素包括(7,1)、(10,3)、(7,4)?因为初始的索引4的值是7,未被处理过。此时,堆顶的元素是(7,1)吗? 是的,因为当堆中的元素是(7,1)和(7,4)时,比较它们的索引,1 <4,所以堆顶是(7,1)。 所以第三次处理索引1,其当前值是否为7?是的。此时,索引1被移除。其左邻居是prev[1] =-1,右邻居是3。所以,处理后的链表结构是: next[-1]不存在,但处理索引1的右邻居是3,所以: next[left] → left是-1,无需处理。 prev[right] → right是3,其prev被设置为-1。索引3的prev变成-1. 然后,索引1的左右邻居的值更新: 左不存在,所以右邻居3的值变为10 +7=17. 所以,堆中插入(17,3)和(如果右邻居存在的话,比如索引3的右是4,那么索引4的值是7+7=14?不,此时索引1的右邻居是3,所以只有右邻居的索引3的值被加7。索引3的值是10+7=17。此时,将(17,3)插入堆。 此时,剩下的节点是索引3和4吗? 是的。索引3的prev是-1(因为其左邻居是1,被移除后,prev[right]被设置为left,即-1。所以索引3的prev是-1。它的next是4。索引4的prev是3。所以链表是3→4。所以,遍历链表时,得到17和7的值,符合样例输出。 所以,该算法是正确的。 现在,编写代码需要注意以下几点: 1. 优先队列的比较函数必须正确,当值相同时,选择索引较小的。 2. 在每次处理节点时,正确更新链表结构,即prev和next数组。 3. 在收集结果时,正确遍历链表结构。 现在,编写代码的大致结构: #include <iostream> #include <vector> #include <queue> using namespace std; struct Compare { bool operator()(const pair<int, int>& a, const pair<int, int>& b) { if (a.first != b.first) { return a.first > b.first; // 小顶堆,a的value大于b时,a的优先级低。所以堆顶是最小的value。 } else { return a.second > b.second; // value相等时,索引大的排在后面,所以堆顶是索引较小的。 } } }; int main() { int N, K; cin >> N >> K; vector<int> A(N); for (int i = 0; i < N; ++i) { cin >> A[i]; } vector<int> prev(N); vector<int> next(N); vector<int> values = A; vector<bool> removed(N, false); for (int i = 0; i < N; ++i) { prev[i] = i - 1; next[i] = i + 1; } prev[0] = -1; next[N-1] = -1; priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> pq; for (int i = 0; i < N; ++i) { pq.push({values[i], i}); } while (K > 0) { auto top = pq.top(); pq.pop(); int val = top.first; int idx = top.second; if (removed[idx] || values[idx] != val) { continue; } // 处理该节点 removed[idx] = true; K--; int left = prev[idx]; int right = next[idx]; // 更新左右邻居的指针 if (left != -1) { next[left] = right; } if (right != -1) { prev[right] = left; } // 更新左右邻居的值 if (left != -1) { values[left] += val; pq.push({values[left], left}); } if (right != -1) { values[right] += val; pq.push({values[right], right}); } } // 收集结果 vector<int> res; // 找到头节点:未被移除的,且prev为-1的节点 int current = -1; for (int i = 0; i < N; ++i) { if (!removed[i] && prev[i] == -1) { current = i; break; } } while (current != -1) { res.push_back(values[current]); current = next[current]; } for (int i = 0; i < res.size(); ++i) { if (i > 0) cout << " "; cout << res[i]; } cout << endl; return 0; } 这样,应该可以正确处理样例。 现在,测试样例输入: 5 3 1 4 2 8 7 初始prev数组是[-1,0,1,2,3], next数组是[1,2,3,4,-1]. 初始堆中的元素是(1,0)、(4,1)、(2,2)、(8,3)、(7,4)。 第一次弹出(1,0):未被移除,values[0]是1。处理: removed[0] = true. left=prev[0]=-1,right=next[0]=1. 更新指针:next[left](不存在)和prev[right] → prev[1] = -1. 所以,此时prev[1] =-1,next[1]=2. 然后,left不存在,所以只处理right=1。values[1] +=1 →4+1=5。将(5,1)入堆。 第二次循环,弹出堆中的下一个元素。此时堆顶可能是(2,2),因为其他元素可能还在堆中。假设弹出(2,2),未被移除,values[2]等于2。处理: removed[2] = true. left=1,right=3. 更新指针:next[1]=3,prev[3]=1. 然后,处理left和 right: values[1] +=2 →5+2=7。插入(7,1). values[3] +=2 →8+2=10。插入(10,3). 第三次循环,弹出堆顶元素。此时堆中有(4,1)(旧值),(5,1)(旧值),(7,1)(新值),(10,3),(7,4)等。当弹出(7,1)时,检查是否有效。此时,values[1]是7,未被移除。处理: removed[1]=true. left=prev[1]=-1,right=next[1]=3. 更新指针:next[left]不存在,prev[right]=-1. 此时,right=3的prev被设为-1. 处理右邻居3的值:values[3] +=7 →10+7=17。插入(17,3). 此时,K=0,循环结束。 收集结果时,找到未被移除且prev为-1的节点。此时,索引3的prev是-1(因为其prev被设置为-1吗?在处理第三次操作时,当处理索引1时,其right是3。此时,prev[3]被设置为left=prev[1]=-1。所以是的。索引3的prev是-1。未被移除,所以current=3。然后next[3]是4。索引4未被移除。所以,收集17(values[3]=17)和7(values[4]=7)。输出17 7,正确。 这样,该代码是正确的。</think>要解决这个问题,我们需要高效地模拟每次移除魔力值最小的宝石并更新相邻宝石的过程。直接模拟会导致超时,因此需要使用优先队列(最小堆)来快速找到最小值,并结合双向链表维护宝石的当前顺序。 ### 方法思路 1. **优先队列**:用于快速找到当前魔力值最小的宝石。队列中存储的是宝石的值和索引。 2. **双向链表**:维护宝石的顺序,以便在移除某个宝石后快速更新相邻宝石的指针。 3. **延迟处理**:每次从优先队列取出元素时,检查其有效性(是否已被移除或值已过时),若无效则跳过,直到找到有效的宝石进行处理。 ### 解决代码 ```cpp #include <iostream> #include <vector> #include <queue> using namespace std; struct Compare { bool operator()(const pair<int, int>& a, const pair<int, int>& b) { if (a.first != b.first) { return a.first > b.first; // 小顶堆,值小的优先 } else { return a.second > b.second; // 值相同时,索引小的优先 } } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N, K; cin >> N >> K; vector<int> A(N); for (int i = 0; i < N; ++i) { cin >> A[i]; } vector<int> prev(N), next(N); vector<int> values(A.begin(), A.end()); vector<bool> removed(N, false); for (int i = 0; i < N; ++i) { prev[i] = i - 1; next[i] = i + 1; } prev[0] = -1; next[N - 1] = -1; priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> pq; for (int i = 0; i < N; ++i) { pq.emplace(values[i], i); } while (K > 0) { auto [val, idx] = pq.top(); pq.pop(); if (removed[idx] || values[idx] != val) continue; removed[idx] = true; K--; int left = prev[idx]; int right = next[idx]; // 更新左右邻居的指针 if (left != -1) next[left] = right; if (right != -1) prev[right] = left; // 更新左右邻居的值并加入堆 if (left != -1) { values[left] += val; pq.emplace(values[left], left); } if (right != -1) { values[right] += val; pq.emplace(values[right], right); } } // 收集结果 vector<int> res; int current = -1; for (int i = 0; i < N; ++i) { if (!removed[i] && prev[i] == -1) { current = i; break; } } while (current != -1) { res.push_back(values[current]); current = next[current]; } for (size_t i = 0; i < res.size(); ++i) { if (i > 0) cout << ' '; cout << res[i]; } cout << '\n'; return 0; } ``` ### 代码解释 1. **输入处理**:读取初始宝石数量和操作次数,以及各宝石的初始魔力值。 2. **双向链表初始化**:维护每个宝石的前驱和后继指针。 3. **优先队列初始化**:将所有宝石的初始值和索引加入队列,确保每次取出最小值。 4. **处理操作**:每次从队列中取出最小值,检查有效性后更新相邻宝石的魔力值和链表指针,并将更新后的值重新加入队列。 5. **结果收集**:遍历链表,输出剩余宝石的魔力值。 该方法通过优先队列和双向链表的结合,高效处理了每次操作,确保在较大数据规模下仍能高效运行。
阅读全文

相关推荐

最新推荐

recommend-type

12月英语网络统考复习资料2-作文范文汇总.doc

12月英语网络统考复习资料2-作文范文汇总.doc
recommend-type

15利用公共密钥基础结构配置网络安全性.ppt

15利用公共密钥基础结构配置网络安全性.ppt
recommend-type

1云金融-任务三-云计算带来的金融变革和发展趋势.pptx

1云金融-任务三-云计算带来的金融变革和发展趋势.pptx
recommend-type

前端分析-202307110078988

前端分析-202307110078988
recommend-type

Delphi 12.3控件之MakerAI-12.rar

Delphi 12.3控件之MakerAI-12.rar
recommend-type

构建基于ajax, jsp, Hibernate的博客网站源码解析

根据提供的文件信息,本篇内容将专注于解释和阐述ajax、jsp、Hibernate以及构建博客网站的相关知识点。 ### AJAX AJAX(Asynchronous JavaScript and XML)是一种用于创建快速动态网页的技术,它允许网页在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容。AJAX的核心是JavaScript中的XMLHttpRequest对象,通过这个对象,JavaScript可以异步地向服务器请求数据。此外,现代AJAX开发中,常常用到jQuery中的$.ajax()方法,因为其简化了AJAX请求的处理过程。 AJAX的特点主要包括: - 异步性:用户操作与数据传输是异步进行的,不会影响用户体验。 - 局部更新:只更新需要更新的内容,而不是整个页面,提高了数据交互效率。 - 前后端分离:AJAX技术允许前后端分离开发,让前端开发者专注于界面和用户体验,后端开发者专注于业务逻辑和数据处理。 ### JSP JSP(Java Server Pages)是一种动态网页技术标准,它允许开发者将Java代码嵌入到HTML页面中,从而实现动态内容的生成。JSP页面在服务器端执行,并将生成的HTML发送到客户端浏览器。JSP是Java EE(Java Platform, Enterprise Edition)的一部分。 JSP的基本工作原理: - 当客户端首次请求JSP页面时,服务器会将JSP文件转换为Servlet。 - 服务器上的JSP容器(如Apache Tomcat)负责编译并执行转换后的Servlet。 - Servlet生成HTML内容,并发送给客户端浏览器。 JSP页面中常见的元素包括: - 指令(Directives):如page、include、taglib等。 - 脚本元素:脚本声明(Script declarations)、脚本表达式(Scriptlet)和脚本片段(Expression)。 - 标准动作:如jsp:useBean、jsp:setProperty、jsp:getProperty等。 - 注释:在客户端浏览器中不可见的注释。 ### Hibernate Hibernate是一个开源的对象关系映射(ORM)框架,它提供了从Java对象到数据库表的映射,简化了数据库编程。通过Hibernate,开发者可以将Java对象持久化到数据库中,并从数据库中检索它们,而无需直接编写SQL语句或掌握复杂的JDBC编程。 Hibernate的主要优点包括: - ORM映射:将对象模型映射到关系型数据库的表结构。 - 缓存机制:提供了二级缓存,优化数据访问性能。 - 数据查询:提供HQL(Hibernate Query Language)和Criteria API等查询方式。 - 延迟加载:可以配置对象或对象集合的延迟加载,以提高性能。 ### 博客网站开发 构建一个博客网站涉及到前端页面设计、后端逻辑处理、数据库设计等多个方面。使用ajax、jsp、Hibernate技术栈,开发者可以更高效地构建功能完备的博客系统。 #### 前端页面设计 前端主要通过HTML、CSS和JavaScript来实现,其中ajax技术可以用来异步获取文章内容、用户评论等,无需刷新页面即可更新内容。 #### 后端逻辑处理 JSP可以在服务器端动态生成HTML内容,根据用户请求和数据库中的数据渲染页面。Hibernate作为ORM框架,可以处理Java对象与数据库表之间的映射,并提供数据库的CRUD(创建、读取、更新、删除)操作。 #### 数据库设计 博客网站的数据库设计通常包含多个表,如用户表(存储用户信息)、文章表(存储文章信息)、评论表(存储用户评论信息)等。使用Hibernate框架可以简化数据库操作,同时确保数据的一致性和安全性。 #### 安全性和性能优化 安全性是构建网站时需要考虑的重要方面,包括但不限于SQL注入防护、XSS攻击防护、会话管理等。性能优化方面,可以利用Hibernate的缓存机制,以及对JSP页面和ajax请求进行适当的缓存处理。 ### 结论 ajax、jsp、Hibernate技术结合可以构建出高效、动态、易于维护的博客网站。在开发过程中,应当关注前后端分离、用户体验优化、系统性能和安全性等关键要素,确保博客网站的稳定和长期可用性。通过本篇文章,读者应该已经对这些技术有了初步了解,并能够结合文件提供的源码开始进行相关开发实践。
recommend-type

【Unity Sunny Land关卡设计高级指南】:打造完美关卡的8大技巧

# 1. Unity关卡设计概述 ## 1.1 关卡设计的核心要素 关卡设计作为游戏开发中不可或缺的一环,承担着构建游戏世界、呈现故事背景、引导玩家体验和激发玩家兴趣的责任。一个优秀的关卡不仅需要有吸引人的视觉效果和有趣的挑战,还需平衡玩家体验,提供连贯的游戏故事。 ## 1.2 关卡设计师的角色 在Unity引擎的环境下,关卡设计师需要具备对场景布局、角色互动、剧情
recommend-type

C++ 模版

<think>我们正在讨论C++模板的使用和常见问题。根据用户要求,所有行内数学表达式使用$...$,独立公式使用$$...$$,用中文回答,并生成相关问题。同时,回答要结构清晰,帮助用户逐步解决问题。 首先,回顾用户的问题:用户想了解C++模板的使用和常见问题。我们将从以下几个方面展开: 1. 模板的基本概念和用途 2. 函数模板和类模板 3. 模板特化和偏特化 4. 模板元编程简介 5. 常见问题及解决方案 注意:在回答中,我们会引用站内提供的引用内容(引用[1]和引用[2])作为参考,但主要围绕模板展开。 ### 1. 模板的基本概念和用途 C++模板是一种支持泛型编程的特性,允许
recommend-type

C#随机数摇奖系统功能及隐藏开关揭秘

### C#摇奖系统知识点梳理 #### 1. C#语言基础 C#(发音为“看井”)是由微软开发的一种面向对象的、类型安全的编程语言。它是.NET框架的核心语言之一,广泛用于开发Windows应用程序、ASP.NET网站、Web服务等。C#提供丰富的数据类型、控制结构和异常处理机制,这使得它在构建复杂应用程序时具有很强的表达能力。 #### 2. 随机数的生成 在编程中,随机数生成是常见的需求之一,尤其在需要模拟抽奖、游戏等场景时。C#提供了System.Random类来生成随机数。Random类的实例可以生成一个伪随机数序列,这些数在统计学上被认为是随机的,但它们是由确定的算法生成,因此每次运行程序时产生的随机数序列相同,除非改变种子值。 ```csharp using System; class Program { static void Main() { Random rand = new Random(); for(int i = 0; i < 10; i++) { Console.WriteLine(rand.Next(1, 101)); // 生成1到100之间的随机数 } } } ``` #### 3. 摇奖系统设计 摇奖系统通常需要以下功能: - 用户界面:显示摇奖结果的界面。 - 随机数生成:用于确定摇奖结果的随机数。 - 动画效果:模拟摇奖的视觉效果。 - 奖项管理:定义摇奖中可能获得的奖品。 - 规则设置:定义摇奖规则,比如中奖概率等。 在C#中,可以使用Windows Forms或WPF技术构建用户界面,并集成上述功能以创建一个完整的摇奖系统。 #### 4. 暗藏的开关(隐藏控制) 标题中提到的“暗藏的开关”通常是指在程序中实现的一个不易被察觉的控制逻辑,用于在特定条件下改变程序的行为。在摇奖系统中,这样的开关可能用于控制中奖的概率、启动或停止摇奖、强制显示特定的结果等。 #### 5. 测试 对于摇奖系统来说,测试是一个非常重要的环节。测试可以确保程序按照预期工作,随机数生成器的随机性符合要求,用户界面友好,以及隐藏的控制逻辑不会被轻易发现或利用。测试可能包括单元测试、集成测试、压力测试等多个方面。 #### 6. System.Random类的局限性 System.Random虽然方便使用,但也有其局限性。其生成的随机数序列具有一定的周期性,并且如果使用不当(例如使用相同的种子创建多个实例),可能会导致生成相同的随机数序列。在安全性要求较高的场合,如密码学应用,推荐使用更加安全的随机数生成方式,比如RNGCryptoServiceProvider。 #### 7. Windows Forms技术 Windows Forms是.NET框架中用于创建图形用户界面应用程序的库。它提供了一套丰富的控件,如按钮、文本框、标签等,以及它们的事件处理机制,允许开发者设计出视觉效果良好且功能丰富的桌面应用程序。 #### 8. WPF技术 WPF(Windows Presentation Foundation)是.NET框架中用于构建桌面应用程序用户界面的另一种技术。与Windows Forms相比,WPF提供了更现代化的控件集,支持更复杂的布局和样式,以及3D图形和动画效果。WPF的XAML标记语言允许开发者以声明性的方式设计用户界面,与C#代码分离,易于维护和更新。 #### 9. 压缩包子文件TransBallDemo分析 从文件名“TransBallDemo”可以推测,这可能是一个C#的示例程序或者演示程序,其中“TransBall”可能表示旋转的球体,暗示该程序包含了动画效果,可能是用来模拟转动的球体(如转盘或摇奖球)。该文件可能是用来展示如何实现一个带有视觉动画效果的摇奖系统的C#程序。 总结以上内容,我们可以得出构建一个C#摇奖系统需要深入理解C#语言及其随机数生成机制,设计用户界面,集成动画效果,确保隐藏控制逻辑的安全性,以及全面测试系统以保证其正确性和公平性。通过掌握Windows Forms或WPF技术,可以进一步增强系统的视觉和交互体验。
recommend-type

【数据驱动的力量】:管道缺陷判别方法论与实践经验

# 摘要 数据驱动技术在管道缺陷检测领域展现出强大的力量,本文首先概述了数据驱动的力量和管道缺陷判别的基础理论。接着,重点探讨了管道缺陷的类型与特征、数据采集与预处理、数据驱动模型的选择与构建。在实践技巧章节,本文详述了实战数据集的准备与处理、缺陷识别算法的应用、以及性能评估与模型优化。此外,高级应用章节深入讨论了实时数据处理分析、多模态数据融合技术、以及深度学习在缺