线段树综合
线段树综合
自从模拟赛出现一道勾石线段树题目开始,命运的齿轮就开始转动。
线段树分裂
之前学过线段树合并,现在又要学线段树分裂了 \(\text{qwq}\)。
线段树分裂意在将线段树维护的信息拆成多个区间进行维护。不难发现,如果直接重新建树是 \(O(n\log n)\) 的,而线段树分裂能够做到 \(O(k\log n)\),\(k\) 是拆分成的区间个数。
在这个基础下,线段树分裂又分为大小拆分和下标拆分。顾名思义,大小拆分是通过给定原线段树在拆分后剩余的大小进行拆分的;而下标拆分则是通过原线段树在拆分后维护的信息所在区间的范围进行拆分的。
代码
void Split_Size(int u,int &v,int k){
if(!u)return ;
v=NewNode();//建立新结点
int a=siz[ls[u]];
if(a<k)Split_Size(rs[u],rs[v],k-a);//如果左区间元素的个数不够要求,那么左区间一定全部在原线段树中,对右区间进行处理即可
else swap(rs[u],rs[v]);//不然右区间一定全部在分裂出的线段树中,将原线段树维护的内容给到新线段树即可
if(k<a)Split_Size(ls[u],ls[v],k);//如果左区间不全是原线段树的,那么需要对左区间进行处理
siz[v]=siz[u]-k;//更新大小
siz[u]=k;
return ;
}//通过大小分裂线段树
void Split_Index(int u,int &v,int l,int r,int p){
if(!u)return ;
v=NewNode();//建立新结点
int mid=(l+r)>>1;
if(mid<p)Split(rs[u],rs[v],mid+1,r,p);//如果原线段树包含整个左区间,那么只需要对右区间进行处理
else swap(rs[u],rs[v]);//不然右区间一定全部在分裂出的线段树中,将原线段树维护的内容给到新线段树即可
if(p<mid)Split(ls[u],ls[v],l,mid,p);//如果左区间不全是原线段树的,那么需要对左区间进行处理
PushUp(u,ls[u],rs[u]);//更新大小
PushUp(v,ls[v],rs[v]);
return ;
}//通过下标分裂线段树
时间复杂度
不难发现,在每一层递归中,都只会调用一层函数,所以复杂度是线段树的层高,也就是 \(O(\log n)\)。
值得注意的是,我的两种写法对于分裂出空线段树会存在空间浪费,也就是空结点的情况,可能会对时空复杂度造成影响,因此对于可能分裂出空线段树的操作最好是特判。
线段树优化建图
线段树优化建图常见于题目中出现多对一或一对多的对应关系时优化正常做法。因为一对多的关系,不仅单独建边会导致空间复杂度过大,而且时间复杂度也无法保证。此时就需要利用线段树能够将一个区间分成最多 \(\log n\) 段,此时,建边的时空复杂度均为 \(O(q\log n)\),\(q\) 为建边关系的个数。接着通过线段树原有的边将关系下放就可以得到正确答案。接着,建图又分为点为起点、区间为终点和区间为起点、点为终点的建边关系。
代码(点 \(\to\) 区间)
void Build(int id,int l,int r){
if(l==r){
idx[l]=id;//记录每一个位置对应在线段树上的位置
return ;
}
int mid=(l+r)>>1;
Build(lid,l,mid);
Build(rid,mid+1,r);
AddEdge(id,lid);//线段树上将关系下放的边
AddEdge(id,rid);
return ;
}
void Insert(int id,int l,int r,int p,int q,int v){
if(p<=l&&r<=q){
AddEdge(v,id);//将对应点和对应区间连接
return ;
}
int mid=(l+r)>>1;
if(p<=mid)Insert(lid,l,mid,p,q,v);
if(q>mid)Insert(rid,mid+1,r,p,q,v);
return ;
}
需要注意的是,上文的代码只适用于以点为起点,区间为终点的建边关系。对于以区间为起点,点为终点的建边关系,需要将线段树上的边反向,进行上抬才可以。
代码(区间 \(\to\) 点)
void Build(int id,int l,int r){
if(l==r){
idx[l]=id;//统计每一个点的对应位置
return ;
}
int mid=(l+r)>>1;
Build(lid,l,mid);
Build(rid,mid+1,r);
AddEdge(lid+S,id+S);//建立上抬关系的边
AddEdge(rid+S,id+S);
return ;
}
void Insert(int id,int l,int r,int p,int q,int v){
if(p<=l&&r<=q){
AddEdge(id,v);//从区间到对应点连边
return ;
}
int mid=(l+r)>>1;
if(p<=mid)Insert(lid,l,mid,p,q,v);
if(q>mid)Insert(rid,mid+1,r,p,q,v);
return ;
}
而对于两种关系同时存在的图,我们需要建两颗线段树,一棵线段树用于下放关系,一棵树用来上抬关系,保证在区间内的单点,能够上升到对应区间中,也就是以第二棵线段树为关系发出的点,第一棵线段树为关系接受的点。同时,两棵线段树之间的单点也是对应的,需要连边保证连通。
代码(两者都有)
void Build(int id,int l,int r){
if(l==r){
idx[l]=id;
S=max(S,id);//确定第二棵线段树编号的偏移量
return ;
}
int mid=(l+r)>>1;
Build(lid,l,mid);
Build(rid,mid+1,r);
return ;
}
void Connect(int id,int l,int r){
if(l==r){
AddEdge(id,id+S);
return ;
}
int mid=(l+r)>>1;
AddEdge(id,lid);//一棵树下放
AddEdge(id,rid);
AddEdge(lid+S,id+S);//一棵树上抬
AddEdge(rid+S,id+S);
Connect(lid,l,mid);
Connect(rid,mid+1,r);
return ;
}
void Insert(int id,int l,int r,int p,int q,int v,int opt){
if(p<=l&&r<=q){
if(opt==0)AddEdge(v+S,id);
else AddEdge(id+S,v);//分情况连边
return ;
}
int mid=(l+r)>>1;
if(p<=mid)Insert(lid,l,mid,p,q,v,opt);
if(q>mid)Insert(rid,mid+1,r,p,q,v,opt);
return ;
}
最后在优化后的图上进行相同的操作即可。
线段树分治
线段树分治一般用于操作有时限性的题目,在这种题目中,可以用线段树分治,每一个节点代表时间的范围,在一个结点的子树内部时就代表该结点对应的操作在当前节点存在,反之则代表不存在。不难发现我们只需要遍历这一颗线段树就可以得到每一个时刻或时间段的结果,而线段树具有良好的 \(O(n)\) 个结点,复杂度十分优秀。接着每一个操作对应的时间段都被拆成 \(O(\log n)\) 个的时间段,因此操作的更新的复杂度是 \(O(q\log n)\) 的。
代码
void Dfs(int id,int l,int r){
int siz=(int)s.size();
for(int i=0;i<(int)T[id].size();i++){//记录结点对应的操作信息
/*
统计该节点的操作对答案的影响
*/
}
if(l==r){
/*
输出统计到的答案
*/
}else{
int mid=(l+r)>>1;
Dfs(lid,l,mid);
Dfs(rid,mid+1,r);//递归继续求解
}
/*
撤销影响
*/
return ;
}
线段树二分
顾名思义,线段树二分就是在线段树上进行二分。利用线段树维护的单调的信息来求解触合理的区间范围进行一系列操作的时候就需要用到线段树二分。但实际上代码非常简单,因为线段树本身就具有折半分割的特点,所以只需要判断左子树是否符合条件,若不符合则递归右子树;反之递归左子树。
线段树二分一般适用于某一个端点是已知的,需要求解另一个端点的情况。
代码
int Find(int id,int l,int r){
if(l==r)return l;
int mid=(l+r)>>1;
//查询左子树信息,并判断是否符合条件
if(true)return Find(lid,l,mid,hgt);//调用左子树继续查找
else return Find(rid,mid+1,r,hgt);//调用右子树继续查找
}
树状数组套权值线段树
众所周知,主席树是静态的,因为一但涉及修改,那么主席树就需要将大量线段树重构,因此复杂度不优。我们不难联想到,主席树的朴素写法类似于前缀和,可以求解区间和问题,而涉及修改时,则可以采用树状数组,因此,我们不妨按照同样的思路,利用树状数组维护主席树所维护的前缀线段树。
那么修改的思路就是将所有树状数组对应的位置修改即可,复杂度是 \(O(\log^2 n)\) 的。查询的时候则是将所有需要查询后累加的位置提前存储,在查询的过程中将所有的位置均向下跳即可,复杂度也是 \(O(\log^2 n)\) 的。
代码
for(int i=x;i<=n;i+=lowbit(i))Insert(r[i],r[i],1,sum,a[x],1);//修改
-------------------------------------------------------------------
int Query(int l,int r){
if(l==r)//返回结果
//统计右端点对应区间和左端点对应区间
int mid=(l+r)>>1;
if(true){//如果左区间满足条件
for(int i=1;i<=cnt1;i++)L[i]=t[L[i]].ls;//全部向左下移
for(int i=1;i<=cnt2;i++)R[i]=t[R[i]].ls;
return Query(l,mid,k);
}else{
for(int i=1;i<=cnt1;i++)L[i]=t[L[i]].rs;//否则全部向右下移
for(int i=1;i<=cnt2;i++)R[i]=t[R[i]].rs;
return Query(mid+1,r,k-cur);
}
}
for(int i=x-1;i>0;i-=lowbit(i))L[++cnt1]=r[i];
for(int i=y;i>0;i-=lowbit(i))R[++cnt2]=r[i];
printf("%d",Query(1,n));//查询
标记拼接
其实很简单,只是对于一些题目,我们不好直接维护需要的信息,那么我们就可以通过一些零碎的标记来共同维护出需要的信息。如山海经中的区间最大子段和,就是通过区间和、靠左的最大子段和、靠右的最大子段和维护出区间最大子段和的。