线段树合并
线段树合并
顾名思义,就是把两颗线段树合并起来变成一颗,可以较为有效地配合动态开点维护权值线段树
线段树合并的基本规则并不难:如果要合并的两颗线段树都有值,就直接把两颗线段树相加,左右子树也分别相加,一直加到叶子节点就行。如果一颗有之另一颗没有,就把空的那棵树指向有值的那颗树。如果两棵树都没有值,退出
这样的方式不能保存历史数据,所以不能持久化
直接看例题:
P3224永无乡
事实上比挂了一个模板标签的雨天的尾巴更纯粹。分析题意,可以发现实际上对于每个点需要维护和它联通的岛和这些岛的排名。连通性可以用并查集维护,至于岛屿的排名可以想到用权值线段树维护。对于每座岛都开一颗动态开点的权值线段树,每次连边操作就合并两颗线段树,查询操作就直接找
具体来说,用并查集维护连通性,利用权值线段树维护排名。对于每一个线段树上的节点,维护一个值val代表当前包含的点数,也就是这座岛联通的岛屿数,同时维护l,r表示区间,son数组存储左右儿子。开一个root数组存储每次操作(建树或合并)产生的新根。对于insert操作,新建节点然后按照对应顺序建出左右子树。对于merge操作,按照合并规则合并,回传新根。对于查询操作,判断要查询的排名x和节点的val的关系,往对应的子树查询直到叶子节点,返回值
详见代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 500010
int n,m,que;
int p[N];//存储排名对应的岛屿
int u,v;
char op;
int x,y;
struct segtree{
int l,r,val;
int son[2];//左右儿子
}t[N*40];
int fth[N];
int root[N],cnt=0;//每个节点开一颗权值线段树
/*并查集维护连通性*/
int find(int x){
if(x==fth[x]) return x;
else return fth[x]=find(fth[x]);
}
/*动态开点权值线段树*/
int insert(int p,int l,int r){
int rt=++cnt;//新建根节点
t[rt].l=l,t[rt].r=r,t[rt].val=1;//初始化参数
if(l==r){
return rt;//叶子节点直接返回
}
int mid=(l+r)>>1;
//更新对应区间的上级线段树的值
if(p>mid) t[rt].son[1]=insert(p,mid+1,r);
else t[rt].son[0]=insert(p,l,mid);
return rt;
}
//线段树合并
int merge(int rt1,int rt2,int l,int r){
if(!rt1&&!rt2) return 0;//找不到就返回0
if(!rt1||!rt2) return rt1+rt2;//只有一侧有值就合并到有值的一侧
int rt=++cnt;//合并后的新根
int mid=(l+r)>>1;
t[rt].l=l,t[rt].r=r,t[rt].val=t[rt1].val+t[rt2].val;//初始化
//分别合并rt1,rt2的左右子树到新节点的左右子树
t[rt].son[0]=merge(t[rt1].son[0],t[rt2].son[0],l,mid);
t[rt].son[1]=merge(t[rt1].son[1],t[rt2].son[1],mid+1,r);
return rt;
}
//统计答案
int query(int x,int k){
if(t[x].val<k) return -1;//x的关联岛不足k座,一定不存在答案,返回-1
if(t[x].l==t[x].r) return p[t[x].l];//叶子节点返回答案
//判断第k大在x的左子树还是右子树,向对应方向进一步搜索
if(t[t[x].son[0]].val>=k) return query(t[x].son[0],k);
else return query(t[x].son[1],k-t[t[x].son[0]].val);
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) fth[i]=i;
for(int i=1;i<=n;i++){
cin>>u;
//给i号岛屿开一颗权值线段树,初始把自己的排名插入
root[i]=insert(u,1,n);
//记录排名对应的岛屿编号,方便找答案
p[u]=i;
}
for(int i=1;i<=m;i++){
cin>>u>>v;
int p=find(u),q=find(v);
if(p!=q){
fth[p]=q;
//并查集维护连通性之后,把连通的两颗线段树合并起来,代表
//原先两棵树各自联通的岛屿均可以一起考虑,同时更新相关信息
root[q]=merge(root[p],root[q],1,n);
}
}
cin>>que;
while(que--){
cin>>op;
cin>>x>>y;
if(op=='Q'){
cout<<query(root[find(x)],y)<<endl;
}else if(op=='B'){
int p=find(x),q=find(y);
if(p!=q){
fth[p]=q;
root[q]=merge(root[p],root[q],1,n);
}
}
}
return 0;
}
还有一道例题:
[Vani有约会] 雨天的尾巴 /【模板】线段树合并
本题是对于某一区间上权值最大的点进行动态维护,所以可以考虑权值线段树
但是由于涉及的权值修改是在一段村庄上同时进行的,所以考虑先去做差分线段树,再将线段树合并求得某个点的权值情况
也就是说,对于每一个村庄,维护一棵权值线段树;在求答案时,将权值线段树合并即可
这里考虑使用动态开点
关于树上差分:
需要求出LCA 在有新的权值加入时需要维护的线段树有左右端点的线段树以及LCA,LCA的父亲的线段树
注意: 应提前为每一个节点的差分线段树的根节点开好点,否则可能会由于在处理差分时未为节点开点导致合并时两棵树的根节点都为0
这样将导致两棵根节点相同的树合并
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 2000010
int n,m,x,y,z;
struct edge{
int to,nxt;
}e[N];//记录原本村庄的道路结构
int head[N],tot,dep[N],f[N][40];//LCA用
struct tree{//每个节点开一颗线段树
int val,id;//存放的救济粮最大值和种类
int ls,rs;//左右儿子的标号
}t[4*N];//对于每一种救济粮构建一颗线段树
int root[N];//根
int cnt;//线段树大小
int ans[N];//答案
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
}
void dfs_edge(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v!=fa){
f[v][0]=u;
dfs_edge(v,u);
}
}
}
void init(){
for(int j=1;(1<<j)<=n;j++){//2的j次幂不大于n
for(int i=1;i<=n;i++){
if(f[i][j-1]){
f[i][j]=f[f[i][j-1]][j-1];
}
}
}
}
int lca(int u,int v){
if(dep[v]<dep[u]) swap(u,v);//保证y是深度更大的点,便于调整
int k=__lg(dep[v])+1;//最大跳跃幂次
int k1=__lg(dep[u])+1;
for(int i=k;i>=0;i--){//x,y调整为同一高度
if(dep[v]-(1<<i)>=dep[u]) v=f[v][i];
}
if(u==v) return u;//若此时xy重合则已经找到lca直接输出
for(int i=k1;i>=0;i--){//一起向上跳
if(f[u][i]!=-1&&f[u][i]!=f[v][i]){
u=f[u][i];
v=f[v][i];
}
}
return f[u][0];//返回
}
void update(int p,bool lc,bool rc){ //依据需要补全子树
if(lc) t[p].ls=++cnt;
if(rc) t[p].rs=++cnt;
}
void push_up(int p){
if(t[t[p].ls].val>=t[t[p].rs].val){
t[p].val=t[t[p].ls].val;
t[p].id=t[t[p].ls].id;
}else{
t[p].val=t[t[p].rs].val;
t[p].id=t[t[p].rs].id;
}
return;
}
void modify(int p,int pos,int k,int l,int r){
if(l==r){
t[p].val+=k;
t[p].id=pos;
return;
}
int mid=(l+r)>>1;
if(pos<=mid){
if(!t[p].ls) update(p,1,0);
modify(t[p].ls,pos,k,l,mid);
}
else{
if(!t[p].rs) update(p,0,1);
modify(t[p].rs,pos,k,mid+1,r);
}
push_up(p);
if(t[p].val==0) t[p].id=0;
}
//线段树合并
void merge(int rt1,int rt2,int l,int r){//l,r为x,y两个节点表示的区间
//若有一个节点为空,直接返回另一方
//if(!rt1||!rt2) return (rt1+rt2);
//如果递归到了叶子节点,直接让值相加
if(l==r){
t[rt1].val+=t[rt2].val;
return;
}
//继续合并子节点:左儿子合并左儿子,右儿子合并右儿子
int mid=(l+r)>>1;
if(t[rt1].ls&&t[rt2].ls) merge(t[rt1].ls,t[rt2].ls,l,mid);//两颗树都有左儿子,合并
else if(t[rt2].ls) t[rt1].ls=t[rt2].ls;//谁有合并后赋值为谁
if(t[rt1].rs&&t[rt2].rs) merge(t[rt1].rs,t[rt2].rs,mid+1,r);//两颗树都有右儿子,合并
else if(t[rt2].rs) t[rt1].rs=t[rt2].rs;
push_up(rt1);
return;
}
//获取答案
void query(int u,int fa){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue;
query(v,u);
merge(root[u],root[v],1,1e5);//合并两间房屋
}
//更新当前节点的ans
if(t[u].val) ans[u]=t[u].id;
else ans[u]=0;
//if(!t[u].val) t[u].id=0;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<n;i++){
cin>>x>>y;
add(x,y);
add(y,x);
}
dfs_edge(1,0);
init();//LCA准备操作
for(int i=1;i<=n;i++) root[i]=++cnt;//给每个点开一颗线段树,等同于建立根
for(int i=1;i<=m;i++){
cin>>x>>y>>z;
int tmp=lca(x,y);
//树上差分:区间[x,y]+1等价于[x]+1,[y]+1,[lca(x,y)]-1,[father(lca(x,y))]-1。
modify(root[x],z,1,1,1e5);
modify(root[y],z,1,1,1e5);
modify(root[tmp],z,-1,1,1e5);
if(f[tmp][0]) modify(root[f[tmp][0]],z,-1,1,1e5);
}
query(1,0);
for(int i=1;i<=n;i++){
cout<</*t[root[i]].id*/ans[i]<<"\n";
}
return 0;
}