线段树进阶
说在前面
这里是一些线段树的进阶运用。例题统一放在末尾。主要参考OI Wiki。
线段树的合并与分裂
前面写过合并分裂,这里再更详细的写一下
前置知识:动态开点
动态开点就是在建树过程中,对于需要的点,就构建,而不需要的就不建,这样可以大大节省空间。由于动态开点抛弃了原有的二叉树的节点编号方式,所以需要另外再开
int tot=1;
void change(int p,int l,int r,int k){
if(!p) return ;
if(l==r) {
t[p].cnt+=1;
return ;
}
int mid=(l+r)>>1;
if(k<=mid){
if(!t[p].ls){
t[p].ls=++tot;
}
change(t[p].ls,l,mid,k);
}
else{
if(!t[p].rs){
t[p].rs=++tot;
}
change(t[p].rs,mid+1,r,k);
}
t[p].cnt=t[t[p].ls].cnt+t[t[p].rs].cnt;
}
int query(int p,int l,int r,int k){
if(!p) return 0;
if(l==r) return 0;
int mid=(l+r)>>1;
if(k<=mid) return t[t[p].rs].cnt+query(t[p].ls,l,mid,k);
else return query(t[p].rs,mid+1,r,k);
}
线段树合并
1.知识点来源
现在给定一棵
看到这个问题,首先想到对每个节点都开一个数组记录第
2.知识点介绍
线段树合并是一种将两个线段树上每个节点的对应信息合并的算法,合并后会得到一棵线段树,其每个节点都保留了原来两棵树上对应节点的信息并,他常常被用于维护树上或图上的路径问题。由于在大部分情况下,相同位置的信息合并只在数域上有意义,所以线段树合并常和权值线段树一起使用。
对于上面的问题,我们不妨先对修改操作进行差分,化为两个点上的修改,接着建立一棵可以在节点间传递的权值线段树,该线段树的区间
线段树合并的过程本质上十分暴力。在合并线段树时,我们不可能每次都新建一棵线段树,这样空间会非常炸裂,所以考虑在原来的一棵树上直接进行合并,过程如下:
假设两棵树为
如果
如果到了叶子节点,合并信息,返回
否则继续递归,回溯时更新节点。
int merge(int pa,int pb){
if(!pa||!pb) return pa+pb;
if(t[pa].l==t[pa].r){
... //do something
return pa;
}
int mid=(t[pa].l+t[pa].r)>>1;
t[pa].ls=merge(t[pa].ls,t[pb].ls);
t[pa].rs=merge(t[pa].rs,t[pb].rs);
pushup(pa);
return pa;
}
上述过程的复杂度为
每个节点的线段树合并后都只记录了该节点子树中的所有信息,设节点为
在过程中有很多浪费了的空间,可以回收起来再进行利用,在线段树分裂时会讲。
3.注意事项
1.注意空间的分配以及点集的大小,某些情况下使用线段树合并会被卡。
2.注意合并的顺序以及对象,上面代码是将
3.不要重复合并同一棵树,这回使时间炸裂。
4.回收空间时回收站大小要开够。
4.常见优化应用
空间回收
线段树分裂时讲,能节省较多空间。
线段树分裂
1.知识点来源
给定一个可重数集,现在有四种操作:
1 p s t 将第
2 p x k 向第
3 p t 将第
4 p l r 询问第
看到上面的问题,假设先不考虑第
现在回到操作
2.知识点介绍
线段树分裂其实是线段树合并的逆过程。线段树分裂只对有序序列有意义,对于无序序列区间并不单调,所以无意义。由于这个性质,线段树分裂常见于权值线段树的维护。
线段树合并和分裂都要进行时注意回收节点,不要出现节点混用/共用的情况。
现在要从管辖区间为
如果当前区间与
如果当前区间与
如果当前区间被
容易证明该过程的复杂度是
void split(int &x,int &y,int l,int r,int s,int t){
if(l>t||s>r) return ;
if(!x) return ;
if(s<=l&&r<=t){
y=x;
x=0;
return ;
}
if(!y) y=recycle();
int mid=(l+r)>>1;
if(s<=mid) split(t[x].ls,t[y].ls,l,mid,s,t);
if(t>mid) split(t[x].rs,t[y].rs,mid+1,r,s,t);
pushup(x),pushup(y);
}
这里说明,由于线段树合并和分裂同时进行时对空间消耗巨大,且会出现点混乱的情况,所以要空间回收。
3.注意事项
1.在分裂和合并同时进行时需要回收空间,以避免点混乱和空间炸裂。
2.分裂时注意if(l>t||s>r) return ;判断,因为有时会神秘的出现这类错误导致
4.常见优化应用
空间回收
建立一个回收站,记录当前空余的位置有哪些,然后在需要时调用,具体代码如下
int recycle(){
if(top) return recycle_bin[top--];
else return ++tot;
}
//invoking function
x=recycle()
//recycle a node
recycle_bin[++top]=x;
clear(x)
吉司机线段树
吉司机线段树常被用于处理区间最值操作和查询区间历史最值。
1.知识点来源
引入
给定一个序列,现在要维护这个序列支持下列操作
1 l r t 将
2 l r 查询
3 l r 查询
抛开操作
引入
给定一个序列,现在要维护这个序列支持下列操作
1 l r x 将区间
2 l r k 求区间
这个问题当然可以使用主席树或平衡树加二分解决,但是这里提供一种玄学的人类智慧做法,其复杂度证明要用到势能分析,本人不会,该做法可以被
考虑维护区间的最大值和最小值,修改操作打个标记即可,而查询操作可以这样做
设当前节点为
如果当前区间的最大值比
如果当前区间的最小值比
否则继续递归
2.知识点介绍
吉司机线段树其实不是一种新的线段树,他只是一种线段树解决区间最值操作和查询区间历史最值的方法。
看引入
sum,maxi,sec,cnt
其中
区间最值操作的流程如下
设当前节点为
如果当前区间与
如果当前区间不被
如果当前区间被
1.如果
2.如果
3.如果
可以用势能分析法证明算法复杂度为
3.注意事项
1.吉司机线段树经常细节很多,注意标记下传的顺序,以及在一些标记下传后,维护的数值会改变。
2.严格次大值,指严格小于最大值的值,而不是
3.某些题卡常,注意结构体线段树常数比数组小。
例题
线段树合并与分裂
线段树合并分裂的例题不太好找,这里只有少数几道
luoguP4556 雨天的尾巴
luoguP5494 线段树分裂
luoguP3224 永无乡
CF208E Blood Cousins
luoguP5384 雪松果树
吉司机线段树
luoguP6242 线段树3
luoguP4314 CPU监控
CF855F Nagini