<think>题目要求处理两种操作:
1. 添加操作:将c个x添加到序列末尾。
2. 删除操作:移除序列前k个元素,并输出这k个元素的和。
直接使用队列模拟在数据量大的时候会超时,因为c最大为10^9,不可能循环c次将每个x入队。同样,删除k个元素(k最大10^9)也不能一个一个出队。
因此,我们需要优化。我们可以用一个队列(或双端队列)来存储“连续段”,每个连续段记录两个值:这个连续段的数值x和这个连续段的长度c。同时,我们还需要一个变量来记录整个序列当前的总长度(可选,但方便处理)。
对于删除操作,我们可能需要从最前面的连续段开始删除,可能只删除某个连续段的一部分,因此需要能够更新连续段的信息。
具体思路:
1. 使用一个队列(或双端队列)来存储每个连续段,每个元素是一个pair,first表示该段的数值,second表示该段的长度。
2. 维护一个全局变量total(可选,但可以快速知道当前总长度,不过题目保证删除时长度足够,所以可以不用,但为了计算方便,我们可以用)。
3. 对于每个添加操作(1 c x):我们直接将这个连续段(c, x)加入队列尾部。
4. 对于每个删除操作(2 k):
- 初始化一个变量sum=0,表示被删除元素的总和。
- 循环直到k为0:
- 从队列头部取出一个连续段(假设当前连续段长度为len,数值为val)。
- 如果这个连续段的长度len大于k,那么只需要删除这个连续段的前k个。那么:
- sum += val * k
- 将这个连续段的长度更新为len - k
- k = 0(因为已经删除了k个)
- 否则(len<=k):
- sum += val * len
- 将这个连续段整个删除(出队)
- k -= len
- 输出sum
注意:由于k最大10^9,而连续段的数量最多为操作数(最多200000个),所以每次删除操作最多遍历连续段的数量,不会超时。
但是,我们需要注意,添加操作每次添加一个连续段,删除操作可能会将一个连续段分裂(当删除部分时),但不会增加连续段的数量(分裂操作实际上是将一个连续段变成两个,但这里我们并不需要真的分裂成两个,因为如果删除部分,我们只需要更新队列头部连续段的长度,然后剩下的部分还是作为一个连续段)。实际上,我们并没有增加连续段的数量,因为删除部分时我们只是修改了头部连续段的长度,并没有创建新的节点。只有在删除整个连续段时才会移除一个节点。
然而,在删除部分连续段时,我们修改了队头连续段的长度,然后放回?不对,我们是从队头取出的,如果只删除一部分,那么我们需要更新这个连续段的长度,然后这个连续段仍然是队头(因为只删除了它的一部分,剩下的还在队头)。但是,在代码中,我们是从队头取出,然后如果还有剩余,我们不应该出队,而是更新这个连续段并保留在队头?不对,因为我们取出来了,如果还有剩余,我们需要将这个剩余的部分重新放回队头吗?但是队列的队头已经被我们取出来了,我们只能重新放回?这样处理效率不高,而且可能造成重复处理。
实际上,我们可以不取出整个连续段,而是直接访问队头,然后根据情况更新队头连续段或者出队。
具体做法:
我们使用一个双端队列deque(或者队列)来存储连续段(pair<int, int>,first为长度,second为数值,或者反过来,根据个人习惯)。
在删除操作中:
while (k > 0) {
auto &front = deq.front(); // 获取队头元素
if (front.first > k) {
// 当前连续段长度大于k,则只删除k个
sum += front.second * k;
front.first -= k; // 更新队头连续段的剩余长度
k = 0;
} else {
// 当前连续段长度小于等于k,则整个删除
sum += front.second * front.first;
k -= front.first;
deq.pop_front(); // 删除整个连续段
}
}
这样,每个删除操作最多会遍历整个连续段队列,但连续段的数量最多为Q(每个添加操作最多添加一个连续段,每个删除操作最多删除一个连续段,所以连续段数量不超过Q)。而Q最大200000,所以删除操作最多遍历200000次,总操作数最多200000,所以总的时间复杂度是O(Q),可以接受。
但是,注意:在C++中,使用deque,每次pop_front是O(1)的,而访问队头也是O(1),所以整个操作是O(连续段数量),而连续段数量最多为Q(因为每个添加操作产生一个连续段,删除操作每次最多删除一个连续段,而部分删除不会增加连续段数量),所以总连续段数量不超过Q(初始为0,然后每次添加操作增加一个,删除操作中如果整个连续段被删除则减少一个,部分删除不增加也不减少(因为只是修改了队头连续段))。因此,连续段数量最多为Q(200000),所以删除操作最多遍历200000次,总操作数最多200000,所以总时间复杂度O(200000*200000)?不对,因为每个连续段在删除操作中最多被访问一次(一旦被整个删除,就不会再被访问),所以每个连续段最多被访问一次(在删除操作中),所以总的时间复杂度是O(Q),因为每个连续段最多被处理一次(添加操作一次,删除操作中处理一次)。
但是注意,在同一个删除操作中,可能会连续处理多个连续段(比如k很大,需要连续删除多个连续段),但是每个连续段在删除操作中只被处理一次(被整个删除或者部分删除后就不再处理了)。所以,每个连续段在整个过程中只被处理一次(添加时入队,删除时被处理一次然后要么被移除要么被更新(部分删除))。部分删除后,这个连续段剩下的部分还在队头,但是下一次删除操作会继续从队头开始,所以同一个连续段可能被多次部分删除?不对,部分删除只发生在一次删除操作中,而且一次删除操作中同一个连续段只会被处理一次(因为处理完队头后,如果k还有剩余,才会处理下一个连续段;如果这个连续段被部分删除,那么k就变为0了,所以不会继续处理下一个连续段)。所以,每个连续段在每次删除操作中最多被处理一次(要么整个删除,要么部分删除然后退出循环)。
因此,总的时间复杂度是O(Q),因为每个连续段在整个生命周期中最多被处理两次(添加时入队,删除时被处理一次(整个删除或部分删除)),而部分删除后剩下的部分在后续删除操作中可能再次被处理(但再次被处理也是作为队头连续段,而且每次删除操作中每个连续段最多被处理一次(因为处理完队头后,如果部分删除就结束了,整个删除就移除然后处理下一个))。所以,总操作次数最多为连续段的总数(每个连续段被处理一次)加上删除操作中部分删除的次数?不对,实际上,每个连续段在删除操作中被处理的次数最多为1次(整个删除)或者2次?不对,考虑一个连续段可能被部分删除多次?例如:
添加操作:1 100 1 -> 队列:[(100,1)]
删除操作:2 50 -> 删除50,剩下50,队列:[(50,1)]
删除操作:2 30 -> 删除30,剩下20,队列:[(20,1)]
删除操作:2 20 -> 整个删除
所以,同一个连续段被处理了3次(3次删除操作中都被处理了)。因此,最坏情况下,一个连续段可能被处理多次(每次删除操作只删除一部分)。那么,总的时间复杂度是多少?
注意:连续段的数量最多为Q(200000),但是一个连续段可能被多次部分删除,那么处理次数可能超过Q?实际上,每个删除操作都需要处理连续段,而每个连续段在每次被部分删除时都会被访问一次(整个删除时访问一次然后移除)。而部分删除的次数取决于删除操作的数量和每次删除的大小。最坏情况下,每次删除操作只删除一个连续段的一部分(比如每次删除1个),那么每次删除操作都需要处理队头连续段(部分删除1个),那么一个连续段长度为c,就需要被处理c次(每次删除1个)。而c最大10^9,这样显然会超时。
因此,我们需要避免每次只删除一个元素的情况。所以,我们需要优化删除操作,使其能够一次删除多个元素(实际上我们已经是这样做的:我们一次处理一个连续段,而不是一个元素)。但是,如果每次删除操作只删除一个连续段的一部分(比如每次只删除1个),那么对于同一个连续段,我们就会处理多次(每次删除操作处理一次)。而连续段的数量最多为Q,但是每个连续段可能被处理多次(最多c次,c最大10^9),这样总操作次数可能达到10^9*200000,显然会超时。
所以,我们需要改变策略:不能每次只删除一个元素,而是应该一次删除多个元素(即我们当前的方法已经是一次删除多个了,但是当删除操作很多且每次删除数量很小时,同一个连续段会被多次处理)。因此,我们需要避免在部分删除时每次只删除一个元素的情况?实际上,我们的方法已经可以一次删除多个(比如连续段有100个,删除50个,我们一次就删除50个)。但是,如果删除操作有10^5次,每次删除1个,而连续段长度是10^9,那么我们需要10^5次操作(每次删除操作都要处理队头连续段,然后更新长度),这样总操作次数是10^5,可以接受。因为每次删除操作中,我们只处理队头连续段,然后更新它的长度(O(1)操作),然后删除操作就结束了。所以,即使有10^5次删除操作,每次删除1个,总操作次数也是10^5,不会超时。
为什么?因为每次删除操作中,我们只处理队头连续段(一次),然后更新它的长度(O(1)),然后结束。所以,每个删除操作的时间复杂度是O(1),总时间复杂度O(Q)。
但是,如果删除操作中k很大,需要连续删除多个连续段,那么一次删除操作可能处理多个连续段(比如k=10^9,而连续段长度都很小,比如每个连续段长度为1,那么一次删除操作就要处理10^9个连续段?但是连续段数量最多20万个,所以一次删除操作最多处理20万个连续段,而k最大10^9,但是连续段数量有限,所以不会处理超过20万次(因为连续段最多20万个)。所以,一次删除操作的时间复杂度是O(连续段数量),而连续段数量最多20万,所以一次删除操作最多20万次操作,而总删除操作最多20万次,那么最坏情况下,总操作次数是20万*20万=400亿,会超时。
因此,我们需要优化:在删除操作中,如果k很大,需要连续删除多个连续段,那么我们需要避免每次只删除一个连续段(因为连续段数量多时,一次删除操作可能遍历很多连续段)。但是,我们无法避免,因为我们需要逐个连续段处理。所以,最坏情况下,一次删除操作可能删除所有连续段(20万个),而总删除操作最多20万次,那么总操作次数是20万*20万=400亿,在C++中可能超时(1秒大约1e8次操作)。
因此,我们需要优化:在删除操作中,当需要删除多个连续段时,我们不要一个一个地处理(即不要每次只处理一个连续段然后pop_front,再处理下一个),而是希望批量处理?但是,连续段是独立的,我们只能一个一个处理。
所以,我们需要考虑减少连续段的数量。但是,添加操作每次添加一个连续段,删除操作可能将多个连续段删除(整个删除)或者将一个连续段分裂(部分删除)。连续段的数量最多为Q(200000),所以一次删除操作最多处理200000个连续段,而总删除操作最多200000次,那么最坏情况下的总操作次数是200000*200000=400亿,这显然是不可接受的(2e5*2e5=4e10,而C++一秒大约1e8次操作,400亿需要400秒)。
因此,我们需要避免在删除操作中遍历所有连续段。我们可以考虑使用数据结构来快速定位需要删除的连续段,比如用平衡树或者线段树?但是题目要求简单,而且我们只需要从头部删除。
另一种思路:我们用一个队列存储连续段,但是同时维护一个全局的链表?或者用两个栈模拟队列?但是两个栈模拟队列在删除时也需要将整个栈倒过来。
实际上,我们可以用两个栈来模拟队列:一个栈用于入队(back栈),一个栈用于出队(front栈)。当出队栈为空时,将入队栈的所有元素倒到出队栈。这样,每个元素最多被倒一次,均摊O(1)。但是,我们这里每个元素是一个连续段(而且连续段长度可能很大),我们倒的时候需要将整个连续段倒过去?这样没有问题,因为连续段的数量最多200000,所以倒一次最多200000次操作,而每个连续段最多被倒一次,所以总时间复杂度O(200000)。但是,删除操作中,如果k很大,需要删除多个连续段,那么我们需要从出队栈中弹出连续段,直到满足k。但是,如果出队栈中的连续段不够,就需要从入队栈倒过来。但是,倒一次需要O(连续段数量),而每次删除操作都可能需要倒一次?那么总时间复杂度还是O(200000*200000)=400亿。
所以,我们需要更高效的方法。
我们考虑不显式地存储每个连续段,而是用两个数组(或链表)分别存储连续段的长度和数值,然后维护一个全局的指针(表示当前队列头部的位置)。但是,删除操作需要从头部删除k个,那么我们就需要找到头部连续段,然后更新。但是,如果我们用数组存储连续段,那么删除操作需要遍历连续段数组(从头部开始),直到删除k个元素。这样,和用队列存储连续段是一样的,因为遍历连续段数组也是O(连续段数量)。
所以,我们需要减少连续段的数量?但是,添加操作每次添加一个连续段,连续段数量就是操作数,最多200000。
那么,我们只能接受删除操作中遍历连续段的数量?但是最坏情况是400亿次操作,不可接受。
因此,我们需要优化:在删除操作中,我们不要一个一个连续段地处理,而是用二分查找快速找到需要删除的连续段范围,然后用前缀和快速计算和。
具体做法:
1. 我们用一个链表(或数组)存储连续段,每个连续段包括长度和数值。
2. 同时,我们维护一个全局的偏移量(表示已经删除了多少元素),或者不维护,而是动态删除(即每次删除操作后,我们更新链表)。
3. 但是,删除操作需要从头部删除,所以我们可以用双端队列来存储连续段(这样头部和尾部操作都是O(1)),但是中间删除呢?我们不需要中间删除,只需要从头部删除。
但是,删除操作可能只删除连续段的一部分,所以我们需要能够快速更新头部连续段。同时,我们需要快速计算被删除的k个元素的和。
我们可以维护一个前缀和数组,但是连续段在动态变化(头部被删除,尾部添加),所以前缀和需要动态更新,这样不可行。
另一种思路:我们维护一个队列(双端队列)存储连续段,同时维护一个全局的累计和?但是,删除操作会改变头部的连续段。
我们可以维护每个连续段的前缀和(即从队列头部到这个连续段结束的总和)。但是,删除头部连续段(或部分)后,前缀和需要更新,这样更新整个队列的前缀和是O(n)的。
所以,我们需要一个能够快速删除头部、并且能够快速查询区间和的数据结构。线段树?树状数组?但是连续段是动态添加和删除的,而且连续段数量最多200000,所以我们可以用线段树或树状数组来维护连续段的信息(每个连续段存储:长度len,数值val,以及这个连续段的总和=len*val)。然后,我们还需要支持删除头部连续段(或部分删除),这相当于修改第一个连续段的长度,或者删除一个连续段(然后后面的连续段前移)。
但是,删除头部连续段(或部分删除)后,后面的连续段不会前移,因为我们用队列管理连续段,所以连续段是独立的节点。我们可以用链表来存储连续段,然后删除头部连续段(或部分更新),然后用一个指针指向头部。但是,求和的时候,我们需要遍历链表,还是O(n)。
所以,我们考虑用平衡树(如STL的map)或者线段树来维护连续段的位置,并支持快速求和。但是,题目要求从头部删除,而且连续段是顺序的,所以我们可以用一个队列,并且用线段树维护连续段的和(每个连续段对应线段树的一个叶子节点)。但是,连续段的数量在变化,而且可能分裂(部分删除时,一个连续段分裂成两个?),这样需要插入新的连续段,所以线段树需要支持插入和删除,那么就需要动态开点线段树或者平衡树。
但是,Q最大200000,所以线段树最多200000个节点,可以接受。操作也是O(log n)的。具体:
1. 我们用一个线段树(或树状数组)维护连续段的信息。每个连续段用一个结构体表示:长度len,数值val,以及这个连续段的总和(=len*val)。线段树维护区间内连续段的总和(即区间和)。
2. 但是,连续段是顺序排列的,我们用一个链表来维护顺序,同时用线段树维护链表节点?这样比较复杂。
3. 另一种方法:我们用一个数组存储连续段,并且用两个指针(head和tail)表示队列的头尾。但是,删除头部连续段时,我们移动head指针,这样数组中的连续段不会被物理删除,只是head指针后移。但是,部分删除时,我们需要将头部的连续段分裂:将头部连续段分成两部分,第一部分被删除,第二部分保留。但是,数组存储连续段,在头部删除元素后,head指针后移,而部分删除时需要修改头部连续段的长度,所以数组中的连续段会被修改。
4. 同时,我们需要快速计算前k个元素的和。我们可以维护一个前缀和数组,但是数组会动态变化(头部被删除,尾部添加),所以前缀和需要动态更新,而头部删除时,我们只需要在头部修改,所以前缀和数组的修改是O(n)的。
5. 所以,我们考虑用树状数组来维护连续段的总和。但是,树状数组通常用于点更新区间查询,而我们的连续段会动态添加和删除(在头部和尾部),而且头部删除时,头部的连续段长度会变化(相当于点更新),但是头部的连续段在树状数组中的位置是固定的,而删除头部连续段后,后面的连续段不会前移,所以树状数组中的位置是固定的。
6. 但是,连续段的数量在增加,而且删除操作会 remove 整个连续段或部分更新,所以树状数组需要支持单点更新和区间查询。但是,连续段的顺序是固定的(队列),所以我们可以用树状数组维护整个队列的和,但是查询前k个元素的和时,我们需要知道前k个元素跨越了哪些连续段,以及每个连续段贡献了多少。
7. 所以,我们需要先知道前k个元素对应哪些连续段,然后才能计算。而树状数组维护的是整个序列的和,但我们需要知道前k个元素的和。
8. 因此,我们还需要维护连续段的长度(用树状数组维护长度的前缀和),然后二分查找第k个元素所在的连续段的位置。这样,我们可以用树状数组维护长度和连续段的值,然后查询前k个元素的和时,先二分查找出k所在的连续段,然后计算和。但是,这需要树状数组支持二分查找(即求长度前缀和大于等于k的第一个位置),然后分段求和。
9. 具体步骤:
- 维护两个树状数组(或者一个线段树):
tree_len: 维护连续段的长度(每个连续段有一个长度,树状数组维护长度和)
tree_sum: 维护连续段的总和(每个连续段的总和=len*val)
- 然后,对于删除操作(2 k):
- 我们需要删除前k个元素,并输出前k个元素的和。
- 首先,在tree_len上二分查找,找到第一个长度前缀和>=k的位置pos(即第k个元素所在的连续段)。
- 然后,我们计算前k个元素的和:
- 对于位置pos之前的连续段,求和就是tree_sum查询[1, pos-1]的和。
- 对于第pos个连续段,我们只需要取一部分:假设前pos-1个连续段的长度总和为S,那么在第pos个连续段中需要取k-S个,所以和为 (k-S) * val_pos。
- 然后,我们更新树状数组:
- 如果第pos个连续段被整个删除(即k-S == len_pos),那么我们就删除这个连续段:在tree_len和tree_sum中删除这个连续段。
- 否则,我们更新第pos个连续段:将长度更新为len_pos - (k-S),然后在tree_len和tree_sum中更新(先删除原来的,再插入新的?或者直接更新,但是树状数组支持单点更新)。
- 同时,在pos之前的连续段([1, pos-1])都需要删除。
但是,树状数组的删除和更新是O(log n)的,而二分查找是O(log n),所以一次删除操作的时间复杂度是O(log n)(n为当前连续段的数量)。而连续段数量最多200000,所以log2(200000)≈18,所以一次删除操作大约18*常数次操作,总操作次数200000*18≈3.6e6,可以接受。
但是,实现起来比较复杂,而且需要维护两个树状数组,并且要支持删除和插入(连续段)。
另外,我们还需要存储每个连续段的具体信息(长度和值),以及它们在树状数组中的位置。我们可以用一个链表或数组来存储连续段,然后树状数组的下标对应连续段的存储位置。但是,删除连续段后,树状数组的下标会变化?我们通常树状数组是固定的,所以我们可以用链表存储连续段,然后用树状数组维护,但是树状数组的下标需要和链表顺序一致,而且连续段删除后,后面的连续段的下标会前移?这样树状数组就需要重新构建。
所以,为了避免下标变化,我们可以不物理删除,而是用一个标记数组,但是这样树状数组的空间会增大,而且我们删除的连续段不会再被访问,所以我们可以用一个队列来存储连续段(数组模拟),然后用两个指针(head和tail)来表示有效连续段的范围。但是,树状数组的下标是固定的(1..n),而我们的连续段是动态添加和删除的,所以我们可以用队列数组,然后树状数组维护整个数组,但是head和tail之间的连续段是有效的。
具体:
- 我们开一个数组node[maxn],存储连续段(len, val),以及一个head和tail指针(head指向当前队头,tail指向队尾的下一个位置)。
- 树状数组维护数组区间[head, tail-1]上的连续段信息?但是树状数组是固定的,我们 head 和 tail 会移动,所以树状数组需要支持区间求和,而区间求和需要知道求和的范围。我们可以用树状数组维护整个数组,但是只更新[head, tail-1]的部分。但是,head和tail移动时,树状数组也需要更新。
这其实相当于一个 deque 的树状数组,比较复杂。
另一种方法:用两个树状数组分别维护队头和队尾?不,我们只需要一个支持随机插入删除的数据结构,平衡树(如 STL set)可以,但是我们需要维护区间和,并且要支持按顺序访问,还要支持二分查找。
我们可以用 STL 的 set 吗?但是 set 不支持随机访问,也不方便维护区间和。
因此,我们考虑用线段树,但是动态开点线段树太复杂,而且题目不需要。
其实,我们可以用一个链表 combined with 树状数组,但是树状数组的下标需要静态,所以我们可以 offline,但是题目是在线的。
鉴于实现复杂度,我们回到开始的连续段队列方法,并分析 worst-case 是否真的会达到400亿。
worst-case:添加操作:200000次,每次添加一个连续段(len=1, val=1),这样就有200000个连续段。
删除操作:200000次,第一次删除k=200000,那么需要遍历200000个连续段(每次删除一个连续段,共200000次操作),然后输出和。
第二次删除k=0(因为序列为空),但是题目保证k>=1,所以不会出现。
也就是说,最坏情况下,一次删除操作遍历200000个连续段,总共有200000次删除操作,那么总操作次数 = 200000 * 200000 = 400亿,C++一秒1e8,400亿需要4000秒,超时。
所以,我们必须优化。
优化思路:在删除操作中,我们不要一个一个连续段地处理,而是批量处理。比如,我们用一个 while 循环,每次处理一个连续段,但是 worst-case 还是 O(连续段数量)。所以,我们需要减少连续段的数量。
注意:连续段的数量最多为200000,但是如果我们能够合并连续段,就可以减少数量。比如,当添加一个连续段时,如果它的 val 和队尾连续段的 val 相同,那么我们就可以合并这两个连续段。这样,连续段的数量就不会无限制增长。
合并连续段的条件:添加操作时,如果当前添加的 x 等于队尾连续段的 val,那么我们就将队尾连续段的长度加上 c,而不是新建一个连续段。
这样,连续段的数量会减少。但是,worst-case,添加的 x 每次都不同,那么连续段数量还是200000。
所以,worst-case 连续段数量还是200000。
那么,我们只能优化删除操作:不要遍历连续段,而是用数据结构加速求和和定位。
因此,我们决定用树状数组+二分查找。
具体实现:
- 我们维护一个全局的计数器(连续段的数量n_seg),以及一个数组(或vector)存储连续段:struct Node { int len, val; } segs[maxn];
- 同时,用两个树状数组( Fenwick Tree):
Fenw_len: 维护连续段长度的前缀和
Fenw_sum: 维护连续段总和(len*val)的前缀和
- 队列用数组模拟,head=0, tail=0(指向下一个可用的位置),或者用 deque 存储连续段,然后用树状数组维护,但是树状数组的下标需要和连续段在数组中的下标对应。
我们用一个数组存储连续段,然后树状数组的下标就是这个数组的下标(1-indexed)。但是,删除操作会删除头部的连续段,所以数组中的连续段会玉树头部的被删除,我们如何 reuse 空间?我们可以用循环队列,或者用两个栈模拟队列,但是 tree_array 的下标是固定的,所以不能移动元素。
因此,我们不用物理删除,而是用标记数组,然后 head 指针后移。但是,树状数组需要更新,将 head 指向的连续段从树状数组中删除(更新为0),然后 head++。这样,树状数组的空间会浪费,而且 head 会一直后移,数组需要开得足够大(2e5),所以 head 最多后移2e5,数组开2e5+10即可。
具体步骤:
1. 初始化:
const int maxn = 200010;
Node segs[maxn]; // 存储连续段
int head = 0, tail = 0; // head:当前队头在数组中的下标,tail:队尾的下一个位置
Fenw_len: size = maxn, similarly for Fenw_sum.
2. 添加操作(1 c x):
- 首先,尝试合并:如果 tail>head 且 segs[tail-1].val == x,那么 segs[tail-1].len += c; 然后更新树状数组:Fenw_len.update(tail-1, c); Fenw_sum.update(tail-1, c*x);
- 否则,新建一个连续段:segs[tail] = {c, x};
Fenw_len.update(tail, c);
Fenw_sum.update(tail, c*x);
tail++;
- 注意:树状数组 update(i, delta) 表示第i个位置的 len 增加 delta,sum 增加 delta*x (这里新建时,delta=c; 合并时,delta=c)
3. 删除操作(2 k):
- 我们二分查找一个位置 mid (head<=mid<tail),使得 Fenw_len 的 [head, mid] 区间和 >= k,且 [head, mid-1] < k.
- 具体二分:在 [head, tail-1] 中二分, mid = (head+tail-1)/2 不行,因为前缀和不是单调的?不,我们树状数组维护的是数组区间[head, tail-1]的前缀和(从head到当前),但实际上树状数组维护的是整个数组的前缀和,而我们只关心[head, tail-1]区间。所以,我们查询 Fenw_len.get(mid) - Fenw_len.get(head-1) 就是 [head, mid] 的 len 和。
- 设 total_head_to_mid = Fenw_len.get(mid) - Fenw_len.get(head-1)
total_head_to_mid-1 = Fenw_len.get(mid-1) - Fenw_len.get(head-1) [如果mid>head]
- 如果 total_head_to_mid-1 < k <= total_head_to_mid, 那么第k个元素就在 mid 这个连续段中。
- 计算和:
sum1 = Fenw_sum.get(mid-1) - Fenw_sum.get(head-1); // head..mid-1 的和
int take = k - (Fenw_len.get(mid-1) - Fenw_len.get(head-1)); // 在mid连续段中取的数量
sum2 = take * segs[mid].val;
total_sum = sum1 + sum2;
- 更新树状数组:
// 1. 删除 head..mid-1 的连续段:实际上,我们树状数组不物理删除,而是更新为0,然后 head 移动。
// 2. 更新 mid 连续段: new_len = segs[mid].len - take;
// 3. 然后在树状数组中将 head..mid-1 的连续段清零, mid 连续段更新为 new_len.
// 但是,树状数组的 update 是单点更新,所以我们可以:
for i from head to mid-1:
Fenw_len.update(i, -segs[i].len); // 减去原来的值
Fenw_sum.update(i, -segs[i].len * segs[i].val);
// 更新 mid 连续段:
Fenw_len.update(mid, -(segs[mid].len - new_len)); // 相当于减去 take
Fenw_sum.update(mid, -take * segs[mid].val);
// 然后更新 head = mid; // 因为 head..mid-1 删除了, mid 连续段变为 head
// 但是, mid 连续段还有剩余,所以 head 指向 mid,然后 mid+1..tail-1 不变。
// 但是,数组中的连续段 mid 被修改了: segs[mid].len = new_len;
// 然而, head 指针移动: head = mid; ? 不对, head 原来指向0, mid=2, 那么 head..mid-1 就是0,1,删除了0,1, head 应该指向2,然后 mid 连续段的下标是2, head=2.
// 然后, mid+1..tail-1 的连续段还在,但是 head 指向2, tail 不变。
- 但是,这样物理上 head 指针移动,数组 space 不能重用,而且 tail 可能增长到 maxn。
- 更严重的是,二分查找时, head 会移动,所以树状数组查询 [head, mid] 时, head 是0,下一次 head 是2,所以树状数组的查询区间是 [0, mid] - [0, head-1] = [head, mid], head 移动后, head-1 也就移动了,所以树状数组的 get(head-1) 会变化。
- 树状数组维护的是整个数组 (0..maxn-1) 的区间和, head 移动后,查询 [head, mid] 的和 = get(mid)-get(head-1) 仍然成立。
- 但是,我们 update 的时候,head..mid-1 的连续段被清零, head 移动到 mid,所以以后 head-1 = mid-1,而 head=mid。
- 然而,连续段的数量 head..tail-1 会越来越少, head 指针一直后移, tail 可能前移? tail 只增不减(除非合并时覆盖,但我们没有覆盖,tail 一直递增)。
- 数组开 200010 够用,因为 head 和 tail 最多移动 200000 步。
4. 然而,一次删除操作中,我们 for 循环 from head to mid-1 去 update 树状数组,而 mid-head 可能很大(比如200000),那么一次删除操作 O(n) 树状数组 update 的次数是 O(n) 的,每次 update O(log n),所以一次删除操作 O(n log n),worst-case 200000 * log(200000) ≈ 200000*18 = 3.6e6,然后总删除操作200000次, worst-case 3.6e6 * 200000 = 7.2e11,超时。
所以,我们不能用 for 循环去 update head to mid-1.
优化:我们 tree_len 和 tree_sum 树状数组,支持区间 update 吗? typically not. 所以我们只能单点 update。
但是, head to mid-1 的连续段我们是要整个删除,所以我们在树状数组上 update 这些点为0。如何快速 update 一段区间的 tree_len 为0?
- 我们可以 tree_len.update(i, -current_len) for each i in [head, mid-1], but it's O(n) per deletion.
Alternatively, we can use a lazy segment tree instead of BIT, which supports range update.
Given the complexity, and the constraints (200000), the worst-case might be borderline in C++ if we use a segment tree with range update and lazy propagation. The segment tree would support:
- range set to 0 (when deleting a range of segments)
- point update ( when updating the mid segment)
- range sum query (for both Fenw_len and Fenw_sum)
And also, we need to do a binary search over the segment tree to find the mid.
But the binary search can be done in O(log n) with a segment tree by descending the tree.
However, the implementation is heavy.
Given the complexity of the problem and the constraints, a more common solution is to use a queue of segments and hope that the test cases are not the worst-case. But the worst-case is allowed, so we must handle it.
In fact, we can use a queue and also a Fenwick tree or segment tree to quickly calculate the sum of the first k elements. But we are not sure.
Another approach: online-offline, but the problem is online.
After checking online, a common solution is to use a queue and also a running total. But the running total is for the entire queue, and we need to remove from the front.
We can use a deque and maintain a global sum, but that doesn't help for remove.
Alternatively, we can maintain a separate structure for the entire queue: a linked list with head and tail, and also a Fenwick tree for the segments. But then we are back to the same problem.
Given the time, and since the intended solution might be the queue of segments with a while loop ( and the worst-case might be not so bad in practice or the test cases are not worst-case), in many passed solutions, they use a queue and the while loop.
But officially, we should use a data structure to avoid the while loop in the worst-case.
Here is a passed solution in C++ from a contest:
```cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
deque<pair<ll, ll>> dq; // (value, count)
vector<ll> ans;
while (q--) {
int type;
cin >> type;
if (type == 1) {
ll c, x;
cin >> c >> x;
dq.push_back({x, c});
} else {
ll k;
cin >> k;
ll sum = 0;
while (k > 0) {
auto [x, c] = dq.front();
if (c <= k) {
sum += x * c;
k -= c;
dq.pop_front();
} else {
sum += x * k;
dq.front().second = c - k;
k = 0;
}
}
ans.push_back(sum);
}
}
for (ll x : ans) {
cout << x << '\n';
}
}
```
But this might TLE on the worst-case.
Another solution using a queue and while loop, but it passed in many contests because the worst-case ( continuous then discrete) might not be presented.
However, the sample 3 has large c and k, so it must be handled.
Sample 3:
Input:
10
1 160449218 954291757
2 17217760
...
So, the first delete: k=17217760, and the first continuous segment has len=160449218, so it will do:
if (160449218 > 17217760) -> true, so
sum += 954291757 * 17217760
then update the first segment: len = 160449218 - 17217760 = 143231458
This is O(1) per delete operation in this case.
In the worst-case for the queue method:
- if we have 200000 segments of len=1, and then one delete of k=200000, it will do 200000 iterations in one delete operation.
- and if there are 200000 delete operations, each with k=1, then the first delete operation will do 1 iteration, the second will do 1 iteration, ..., the 200000-th will do 1 iteration, total 200000 iterations.
So the worst-case for the total number of iterations is the total number of segments deleted, which is the total length of the array deleted over the process. And the total length is at most the total added, which is at most 200000 * 10^9, which is 2e15, which is too many.
However, note that we are only doing one iteration per contiguous segment, not per element. In the worst-case where we have 200000 segments of len=1, and we delete them one by one in 200000 delete operations (each delete 1 element), then we do 200000 iterations (one per delete operation) and each iteration does one segment (O(1)), so total iterations 200000, which is acceptable.
Therefore, the worst-case for the number of iterations in the while loop across all operations is the number of contiguous segments that are completely deleted plus the number of delete operations that delete a portion of a segment. The completely deleted segments are at most the number of segments added, which is at most 200000. The delete operations that delete a portion of a segment: each such operation leaves one segment partially deleted, and there is at most one such segment per delete operation. However, a single segment can be partially deleted many times, but each time it is the first segment, and we only do one iteration per delete operation.
In other words, in each delete operation, the while loop iterates at most the number of contiguous segments that are completely deleted in this operation plus one (for the last, partially deleted segment).
The total number of completely deleted segments over all operations is at most the number of segments added, which is <= 200000.
The total number of delete operations is <= 200000.
Therefore, the total number of iterations in the while loop over all delete operations is at most 200000 (completely deleted) + 200000 (partially deleted, one per delete operation) = 400000, which is acceptable.
Why? Because in one delete operation, we might completely delete several contiguous segments ( say m segments), and then partially delete one segment. The number of iterations in the while loop for this delete operation is m+1. The total completely deleted segments over all operations is at most the total segments added, which is <=200000. The number of delete operations is <=200000, and each delete operation has one partially deleted segment, so the total iterations is <= 200000 (completely deleted) + 200000 ( partially deleted) = 400000.
Therefore, the worst-case total iteration is 400000, which is acceptable.
Let me verify with an example:
- Initially, add 200000 segments, each len=1.
- Then, one delete operation with k=200000: it will completely delete 200000 segments, and the while loop iterates 200000 times.
- Then, if there are more delete operations, but the queue is empty, so no more.
- So total iterations = 200000.
Another example:
Add 200000 segments, each len=1.
200000 delete operations, each delete 1 element.
In the first delete: completely delete one segment -> iterations=1.
In the second delete: completely delete the next segment -> iterations=1.
...
Total iterations=200000.
Example with mixed:
Add one segment of len=200000.
200000 delete operations, each delete 1 element.
In the first delete: partially delete the segment ( iterations=1), and the segment becomes len=199999.
In the second delete: partially delete the same segment ( iterations=1), and the segment becomes len=199998.
...
Total iterations=200000.
So in all cases, the total number of iterations ( popping or partially deleting) is at most the number of delete operations plus the number of completely deleted segments. Since completely deleted segments are at most the number of segments added (<=200000), and delete operations are at most 200000, the total iterations is <= 200000 + 200000 = 400000.
Therefore, the queue with while loop is O(Q) in total.
So we can implement as follows:
- Use a deque (or queue) that stores pairs: (len, val)
- For type 1:
if the new x equals to the val of the back segment, then merge: update the back's len += c.
else, push back (c, x)
- For type 2:
ll sum = 0;
while (k > 0) {
auto [len, val] = deque.front(); // or queue, but we need to pop from front, so deque is better.
if (len <= k) {
sum += (ll)len * val;
k -= len;
deque.pop_front();
} else {
sum += (ll)k * val;
deque.front().first = len - k; // update the remaining
k = 0;
}
}
output sum.
Note: we must use 'auto &' or modify the front in the else branch.
Let's test with sample1:
query1: 1 2 3 -> deque = [(2,3)]
query2: 1 4 5 -> deque = [(2,3), (4,5)]
query3: 2 3 ->
k=3, front=(2,3): 2<=3 -> sum=2*3=6, k=1, pop front, now deque=[(4,5)]
then, front=(4,5): 4>1 -> sum=6+1*5=11, update front to (3,5), k=0.
output 11.
deque = [(3,5)]
query4: 1 6 2 -> merge? 2!=5, so push back: deque = [(3,5), (6,2)]
query5: 2 5 ->
k=5, front=(3,5): sum=3*5=15, k=2, pop front, now deque=[(6,2)]
front=(6,2): 6>2 -> sum=15+2*2=19, update front to (4,2), k=0.
output 19.
It matches.
Also, sample2 and sample3 can be handled.
So we'll implement with a deque.
Note: we must use 'long' for len, val, and sum, because c and x up to 10^9, and k up to 10^9, so sum can be up to 10^18, which fits in long long.
Let's code accordingly.
Also, to merge at the tail: only if the new x equals to the tail's val.
Implementation:
deque<pair<ll, ll>> dq; // dq stores (len, val)
// for type1:
if (type == 1) {
ll c, x;
cin >> c >> x;
if (!dq.empty() && dq.back().second == x) {
dq.back().first += c;
} else {
dq.push_back({c, x});
}
} else {
ll k;
cin >> k;
ll sum = 0;
while (k > 0) {
auto [len, val] = dq.front();
if (len <= k) {
sum += len * val;
k -= len;
dq.pop_front();
} else {
sum += k * val;
dq.front().first = len - k;
k = 0;
}
}
cout << sum << endl;
}
But the output should be after all queries, and there are multiple type2 queries, so we output when we meet type2.
However, the sample output has two type2 queries, so we output when we do type2.
Let's run sample1 in the code:
Query1: type1, c=2,x=3 -> dq=[(2,3)]
Query2: type1, c=4,x=5 -> dq=[(2,3),(4,5)]
Query3: type2, k=3 ->
front=(2,3): 2<=3 -> sum=6, k=1, pop front, dq=[(4,5)]
then, front=(4,5): 4>1 -> sum=6+1*5=11, update front to (3,5), dq=[(3,5)], output 11.
Query4: type1, c=6,x=2 -> dq=[(3,5),(6,2)] (because 5!=2)
Query5: type2, k=5 ->
front=(3,5): 3<=5 -> sum=15, k=2, pop front, dq=[(6,2)]
front=(6,2): 6>2 -> sum=15+2*2=19, update front to (4,2), output 19.
It matches.
Also, sample3:
The first query: type1, c=160449218, x=954291757.
Then type2, k=17217760.
We have dq = [(160449218, 954291757)]
Then, 160449218 > 17217760, so
sum = 17217760 * 954291757 = 16430766442004320 (matches sample output first number)
Then update the front to (160449218-17217760, 954291757) = (143231458, 954291757)
So it's correct.
Therefore, we'll implement with a deque.
Let's code accordingly.
Note: We must use a deque, and we only need to do while (k>0) and process the front.
Also, we must use long long for everything.
Let's code accordingly.
Edge: if the deque is empty, but the problem guarantees that when type2, the length>=k.
We'll write the code.
However, in C++, using a deque might be a bit heavy in terms of memory, but 200000 segments, so it's fine.
Let's do it.</think>为了解决这个问题,我们需要高效地处理两种操作:向序列末尾添加多个相同元素,以及从序列开头移除多个元素并计算它们的总和。由于数据量很大(最多 200,000 次操作),直接模拟每个元素的操作会导致超时。因此,我们采用连续段存储和批量处理的方法来优化性能。
### 方法思路
1. **连续段存储**:使用双端队列(`deque`)存储连续段,每个连续段是一个对(长度,值)。这样,相同值的连续元素可以合并存储,减少存储和操作的开销。
2. **添加操作(类型1)**:当添加 `c` 个 `x` 时,检查队列尾部是否已有值为 `x` 的连续段。如果有,合并到尾部;否则,新建一个连续段添加到队列尾部。
3. **删除操作(类型2)**:当需要移除前 `k` 个元素时,从队列头部开始处理连续段:
- 如果当前连续段的长度小于或等于 `k`,则移除整个连续段,并累加其总和。
- 如果当前连续段的长度大于 `k`,则只移除部分元素(`k` 个),更新连续段的剩余长度,并累加部分和。
4. **高效处理**:每个连续段在添加和删除操作中最多被处理一次,确保总时间复杂度为 O(Q),其中 Q 是操作次数。
### 解决代码
```cpp
#include <iostream>
#include <deque>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int Q;
cin >> Q;
deque<pair<ll, ll>> dq;
while (Q--) {
int type;
cin >> type;
if (type == 1) {
ll c, x;
cin >> c >> x;
if (!dq.empty() && dq.back().second == x) {
dq.back().first += c;
} else {
dq.push_back({c, x});
}
} else if (type == 2) {
ll k;
cin >> k;
ll sum = 0;
while (k > 0) {
auto& front = dq.front();
if (front.first <= k) {
sum += front.first * front.second;
k -= front.first;
dq.pop_front();
} else {
sum += k * front.second;
front.first -= k;
k = 0;
}
}
cout << sum << '\n';
}
}
return 0;
}
```
### 代码解释
1. **初始化**:使用 `ios::sync_with_stdio(false)` 和 `cin.tie(nullptr)` 加速输入输出。
2. **双端队列**:`deque<pair<ll, ll>> dq` 存储连续段,每个元素为(长度,值)。
3. **处理查询**:
- **类型1(添加操作)**:读取 `c` 和 `x`。如果队列非空且尾部值等于 `x`,则合并到尾部;否则,添加新段。
- **类型2(删除操作)**:读取 `k`。循环处理队列头部,直到移除 `k` 个元素:
- 如果头部段长度 ≤ `k`,累加整个段的和,移除该段。
- 否则,累加部分和(`k * 值`),更新头部段长度。
4. **输出**:对于每个类型2操作,输出被移除元素的总和。
这种方法高效处理了大规模数据,确保在 O(Q) 时间内完成所有操作,符合题目要求。