题意
维护一个序列,支持插入序列、区间删除、区间覆盖、区间翻转、区间求和、求序列最大子段和(至少包含一个元素)。
插入总数\(\le 4\times 10^6\),任意时刻序列长度\(\le 5\times 10^5\)。
题解
\(Splay\)板子题。
不会\(Splay\)的同学出门左转文艺平衡树。
由于插入数量太大,直接开4000000的数组显得不现实,我们考虑建立回收栈(雾),删除时遍历子树,回收被删除的节点,新建节点时优先从回收栈里取点,但是要注意各个数组都要初始化。
总体思路是,对每个节点维护:\(val,sz,sum,la,ra,ma\),分别表示这个点本身的值、子树大小、子树的\(val\)之和、该区间(以这个点为根的子树所表示的区间,下同)中包含最左边元素的最长子段和(可以不包含元素)、包含最右边元素的最长子段和(可以不包含元素)、整个区间的最大子段和(至少包含一个元素,即答案);懒标记\(cov=0/1,rev=0/1\),分别表示该区间是否被覆盖、是否被翻转。
接下来我们仔细分析每一步操作。
新建节点
已经讲过,若回收栈里有点,则优先拿来用,否则新建节点。
1 | int new_node(int _val){ |
上传
主要是\(la,ra,ma\)的更新。\(la\)和\(ra\)同理,以\(la\)为例,当前节点的\(la\)值就是max(左子树的la,左子树的sum+当前节点的val+右子树的la)
,分别表示跨过当前节点和不跨过当前节点两种情况。对于\(ma\),也差不多,不跨过的情况是max(左子树的ma,右子树的ma)
,跨过的情况是左子树的ra+当前节点的val+右子树的la
,在这两种情况中再取个\(max\)即可。
1 | void up(int u){ |
下传懒标记
比较简单,不需要考虑标记下传的顺序。
对于\(cov\)标记,直接把左右儿子的\(val\)设为当前节点的\(val\),\(sum\)设为\(val\times sz\)。而\(la,ra,ma\)则需要分类讨论,若\(val>0\),显然直接把整个区间选上更优,否则\(la,ra\)不选,\(ma\)只选一个点。
对于\(rev\)标记,直接交换左右儿子的左右子树、左右儿子的\(la,ra\)(注意不是交换当前节点的左右子树、\(la,ra\),这种写法在文艺平衡树可以过,但是在本题中会出错)。
1 | void down(int u){ |
建树
直接按照原序列的顺序建树(注意不是按数的大小建树)。
可是直接插入是\(\mathcal O(n\log n)\)的,且常数较大。我们直接取序列中点作为根,然后递归调用左边和右边,分别作为根的左儿子和右儿子。
这样我们可以做到\(\mathcal O(n)\)建树,\(n\)是序列长度。
1 | int build(int l, int r, int *a){ |
Splay的基本操作:\(rotate(x)\)和\(splay(x,g)\)
\(rotate(x)\)表示将\(x\)向上旋转,\(splay(x,g)\)表示将\(x\)旋转到\(g\)的儿子。
注意\(rotate(x)\)时需要上传操作,且要注意操作顺序。\(splay(x,g)\)需要双旋。
1 | int dir(int x){ return son[fa[x]][1] == x; } // 返回x是他父亲的哪个儿子 |
求当前序列中第\(k\)个位置在Splay中对应的节点
这是下面前五个操作中必需的一个操作,即把序列中的位置转化为树上的节点编号。
这是一个经典的求第\(k\)小的问题,直接按照左子树的\(sz\)和当前的\(k\)的大小关系决定往左、往右还是直接返回当前节点。
注意,此时由于需要用到儿子的信息,我们必须把\(u\)的标记下传。
且因为这样做会把根到要求的那个节点的路径上的所有节点都\(down\)一遍,所以在其余操作中不需要再进行\(down\)操作。
1 | int kth(int u, int k){ |
插入操作
对于插入操作,我们直接把\(pos\)对应的节点\(splay\)到根上(记为\(u\)),把\(pos+1\)的位置旋转到根下面(记为\(v\),即此时\(fa[v]=u\))。此时\(v\)的左子树一定为空(不存在一个整数\(x\)满足\(pos<x<pos+1\))。那么直接把需要插入的序列\(\mathcal O(n)\)建树,把根节点连到\(v\)上,作为\(v\)的左子树即可。
1 | void insert(int x, int tot, int *a){ |
删除操作
假设我们要删除的是区间\([l,r]\),那么同理,我们把\(l-1\)对应的节点旋转到根上(记为\(u\)),把\(r+1\)的位置旋转到根下面(记为\(v\),即此时\(fa[v]=u\))。
此时\(v\)的左子树所表示的区间即为\([l,r]\),那么我们直接把\(v\)的左子树删除即可。
1 | void recycle(int u){ // 回收以u为根的子树 |
覆盖操作
与删除同理,提取区间\([l,r]\),打上\(cov\)标记,更新即可。
1 | void cover(int l, int r, int c){ |
翻转操作
同理,提取区间后,打上\(rev\)标记并更新。
1 | void reverse(int l, int r){ |
求和操作
提取区间后直接输出\(sum\)值即可,不需要更新。
1 | void query_sum(int l, int r){ |
最大子段和
直接输出\(ma[rt]\)即可。
一些细节
- 由于\(pos,l-1\)可能会\(<1\),\(pos+1,r\)可能会\(>n\),所以要考虑边界问题。我的代码中是直接把整个数组向右移动一位,并且使\(a[1]=a[n+2]=-\text{INF}\)。个人觉得这个方法比较简单,只要在主程序中做一些简单的处理即可。
- 由于\(up\)中没有判断左儿子或右儿子为空的情况(判起来会变得很鬼畜),所以我们令\(ma[0]=-\text{INF}\)。
代码实现
总的再发一次吧。吸氧后最快的一次共1111ms。评测记录
1 |
|
如发现代码有问题请在评论中指出,谢谢!