线段树合并学习笔记
前言
之前写过一篇类似于线段树入门的文章,欢迎 dalao 来踩。
主要内容:线段树合并。
本来是想写一写 \(kzw\) 线段树之类的,感觉还是先咕着吧。
启发式合并什么的也先咕着。
前置知识
主要是动态开点线段树与权值线段树。
如果对于两者都有了解的同学可以直接跳过。
动态开点线段树
其实如果学的是结构体线段树的同学,那么动态开点线段树理解起来就非常简单了。
动态开点的线段树与普通线段树的唯一不同就是对于节点 \(p\),它的左右儿子不一定是 \(p\times 2,p\times 2+1\)。
因此我们不得不建立变量记录左右儿子的节点编号(相当于指针)。
同时我们也不能保证每个节点在递归时都一定有左右儿子,所以要不断的动态开点。
具体代码大概长这样:
//假设我们要将 i 号线段树中的位置 k 增加 val。
//rt[] 记录的是根节点的编号。
int update(int p,int l,int r,int k,int val){
if(!p) p=tot++;//动态开点。
//下面就和普通线段树一样了。
if(l==r){t[p].dat+=val;return p;}
int mid=(l+r)>>1;
if(k<=mid) t[p].l=update(t[p].l,l,mid,k,val);
else t[p].r=update(t[p].r,mid+1,r,k,val);
push_up(p);
return p;//返回节点编号。
}
rt[i]=update(rt[i],1,n,k,val);
权值线段树
对于一个给定的数组,普通线段树可以维护某个子数组中数的和。
而权值线段树的作用是维护某个区间内数组元素出现的次数。
当数组元素的值域特别大时常常要离散化或者动态开点来节省空间。
权值线段树可以干的事情有:查询第 k 大,查询前驱后继等一些奇奇怪怪的事情。
这里只给出简单的介绍,学有余力的同学可以自行百度。
代码实现什么的几乎和普通线段树没区别,想知道的可以康康这篇文章。
线段树合并
主角登场。
一般的线段树合并都是权值线段树合并,同时都要动态开点防止空间爆炸。
会了这两个前置知识后就可以乱搞了。
主要思路是这样:
同步遍历两棵线段树并合并,假设我们现在合并到了两棵线段树 \(a,b\) 的 \(pos\) 位置,那么
如果 \(a\) 有 \(pos\) 位置,\(b\) 没有,那么新的线段树 \(pos\) 位置赋成 \(a\),返回。
如果 \(b\) 有 \(pos\) 位置,\(a\) 没有,赋成 \(b\),返回。
如果此时已经合并到叶子节点了,就把 \(b\) 在 \(pos\) 的值加到 \(a\) 上,把新线段树上的 \(pos\) 位置赋成\(a\),返回。
递归处理左子树。
递归处理右子树。
用左右子树的值更新当前节点。
将新线段树上的 \(pos\) 位置赋成 \(a\),返回。
代码大概长这样:
int merge(int a,int b,int l,int r){
if(!a) return b;
if(!b) return a;
if(l==r){
//按照所需合并
return a;
}
int mid=(l+r)>>1;
tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
push_up(a);
return a;
}
主要思路就是这么一点东西。
时空复杂度
这么一通简单的合并下来复杂度是多少呢?
经过百度严谨的证明,我们可以发现它的时空复杂度均为 \(O(n\log n)\),可以说是非常优秀了。
一般在 \(n=5\times10^5\) 的情况下复杂度优秀,但一旦超过这个值就要慎重考虑了。
值得注意的是虽然平衡树的时间复杂度为 \(n\log^2 n\),但是由于线段树合并常数巨大,所以效率往往比平衡树低。
常见优化
需要注意的是在确保时间复杂度的情况下线段树合并对空间的要求很高,所以有一些常用的优化。
一个常见的空间优化就是对于线段树 \(A,B\),直接将 \(B\) 合并到 \(A\) 上从而节省空间。
但是统计答案时因为 \(B\) 的结构可能已经被破坏,所以要在线实时统计答案。
另一个优化被称为垃圾回收站(好像是这样叫的)是这样的:
可以发现每次合并都会删掉一部分节点,那么这些节点显然再也不会用到。
那么我们用一个数组统计这些“再也没用”的节点编号,然后在动态开点时优先考虑从“回收站”中取出编号。
这样同样可以达到空间上的优化。
例题
Lomsat gelral
题意:给定一棵树,每个节点都有一种颜色,求以每个节点为根的子树最多的颜色的节点编号的和。
首先对于每一个节点建立权值线段树。
每棵线段树维护区间最大值,然后就是从下往上合并同时在线统计答案就是了。
雨天的尾巴
每次给树上路径 \((u,v)\) 上的每个节点加上一个 \(z\) 类的物资,求每个节点数量最多的物资的种类。
貌似是模板题
同样是维护区间最大值,可以将每条路径 \((u,v)\) 分为 \((u,lca),(v,lca)\) 两段。
那么就可以用差分将区间修改变为单点修改,具体做法是:
将 \(u,v\) 点的 \(z\) 位置加 \(1\),同时 \(lca,fa[lca]\) 分别减 \(1\)。
然后就是每个点直接查询最大值。
永无乡
题意:给定合并两个连通块的操作,查询连通块内的第 \(k\) 大。
维护无向图的连通块肯定是用并查集啦,查询第 \(k\) 大就用权值线段树。
具体方法是如果 \(k\leq sum[ls]\) 就递归进入左子树,否则令 \(k-=sum[ls]\) 然后递归进入右子树。
每次令 \(fa[y]=x\) 的同时将 \(y\) 合并到 \(x\) 上即可。
天天爱跑步
题意:给定一颗树和 \(m\) 条路径,同时第 \(i\) 个点给定 \(W_i\),求有多少条路径的第 \(W_i+1\) 个点是这个点。
设有路径 \((u,v)\),我们还是可以将其拆成两段:\((u,lca),(v,lca)\)。
考虑利用树上差分将区间操作改变为单点操作。
我们的主要思路是利用 \(w[i]\) 追溯某条路径的出发点,那么只要求有多少个路径的出发点是这个即可。
对于 \((u,lca)\) 这段路径直接将 \(u\) 的线段树的 \(dep[u]\) 位置加 \(1\),之后的节点查询有多少个 \(dep[i]+w[i]\) 即可。
对于 \((v,lca)\) 考虑将 \(u\) 沿 \(lca\) 翻折使 \(dep[u]=dep[lca]-(dep[u]-dep[lca])=dep[lca]\times 2-dep[u]\)。
因为翻折之后等价于经过 \(lca\) 之后向下走到 \(u\)。
之后将线段树 \(v\) 的 \(dep[u]\) 位置加 \(1\) 即可,同时记得将 \(lca\) 和 \(fa[lca]\) 的相应位置都减 \(1\),顺序随便。
同时我们可以观察到 \(dep[lca]\times 2-dep[u]\) 可能为负,所以将值域整体平移 \(n\) 即可。
ROT-Tree Rotations
每次交换不会影响左右子树内部的逆序对个数,所以只考虑左右子树之间的逆序对个数。
至于计算则没有什么难度,由于我们维护的是值域,因此左儿子必定比右儿子大。
那么我们就用左儿子大小乘以右儿子大小得出交换前逆序对个数。交换后同理之。
Peaks
考虑离线做法,将边按照难度从下到大排序,同时询问也一样,每次都合并那些难度不超过当前难度的边。
然后就是并查集维护连通块+权值线段树查询第 \(k\) 大的问题了。
但是难度至于较大(\(a\leq 10^9\)),所以还要提前离散化。
结语
线段树合并长用于维护一些权值线段树能干的,但是又要多次维护的事情。
同时也可以维护一些平衡树和主席树能干的事情。
所以遇到这种题目时可以往线段树合并方面想想。
文章权值线段树实现部分引自这篇文章,在此鸣谢&%作者。
完结撒❀。