线段树合并、分裂
基本概念:
如果需要维护许多个大小为 \(10^5\) 级别的多重集,可以看做给每一个多重集建立一棵线段树。线段树的合并、分裂就是多重集的累加、分开。
这里使用动态开点的方式存储线段树树。
如果一个节点为空,那么它的编号为 \(0\) 。
变量释义:
-
有 \(cnt\) 个多重集
-
建立了 \(tot\) 个节点
-
若一个多重集的编号为 \(x\) ,它的根节点编号为 \(root[x]\)
注意:空间是个谜!能开多大是多大
update:经过 ZCETHAN 的严格证明,空间大小开到 \(O(n\times (\left\lceil \log n\right\rceil+1))\) 足矣。
线段树合并
把以 \(y\) 为根的线段树合并到以 \(x\) 为根的线段树:
int merge(int x,int y,int nl,int nr) // i:y->x
{
if(!x || !y) return x+y;
int mid=(nl+nr)>>1;
//tree[x].sum+=tree[y].sum; 根据题目改动
tree[x].ls=merge(tree[x].ls,tree[y].ls,nl,mid);
tree[x].rs=merge(tree[x].rs,tree[y].rs,mid+1,nr);
//pushup(x);
del(y);
return x;
}
复杂度 \(=\) 节点数 ( 一般均摊下来可以达到一次操作 \(O(\log n)\) 的级别 )
线段树分裂
把以 \(x\) 为根的线段树中 \(\ge k\) 的数转移到一棵 空的 线段树 \(y\) 。
void split(int &x,int &y,int nl,int nr,int k) // i>=k i:x->y
{
if(!x) x=++tot;
if(!y) y=++tot;
if(nl==nr) { swap(x,y); return; }
int mid=(nl+nr)>>1;
if(mid>=k)
{
swap(tree[x].rs,tree[y].rs);
split(tree[x].ls,tree[y].ls,nl,mid,k);
}
else split(tree[x].rs,tree[y].rs,mid+1,nr,k);
pushup(x),pushup(y);
}
update:线段树合并复杂度证明
我们知道对于 \(n\) 个点的启发式合并时 \(\mathcal{O(n\log n)}\) 的。
在一次合并的过程中,我们需要访问的点必定是两棵树重合的部分,这些点的数量一定小于两棵树中较小的那个。
那么就相当于将小的树合并到大的树中去,说明线段树合并属于启发式合并的一种,证明它的复杂度为 \(n\log n\)。
推广:类似 Trie 树合并等的复杂度也可以用上面的方法证明是 \(n\log n\) 的。
例题:
P5494 【模板】线段树分裂 \(\rightarrow\) 模板代码