[学习笔记] 线段树合并
线段树合并,从名字上就可以看出,它是合并两颗线段树的算法,其核心就是动态开点和 merge
函数,而 merge
函数主要有两种写法,两种写法都对应这不同的清况:
首先我们假设有两棵要合并的线段树1和2,相应的结点分别为a和b
- 把b合并到a上
void merge(int &a,int &b,int l,int r) {
if(!a||!b){a+=b;return;}//只要有一个点是残缺的就都看作合并到a
if(l==r) {maxx[a]+=maxx[b];res[a]=l;return;}
int mid=(l+r)>>1;
merge(ls[a],ls[b],l,mid),merge(rs[a],rs[b],mid+1,r);
maxx[a]=max(maxx[ls[a]],maxx[rs[a]]);
res[a]=maxx[a]==maxx[ls[a]]?res[ls[a]]:res[rs[a]];
}
这种情况适用于离线,因为我们在操作的时候可能会破坏线段树2的结构
- 新开一个结点
int merge(int a,int b,int x,int y) {
if(!a||!b){a+=b;return a;}
int root=++tot;
if(l==r) {maxx[root]=maxx[a]+maxx[b];res[root]=l;return root;}
int mid=(l+r)>>1;
ls[root]=merge(ls[a],ls[b],l,mid);
rt[root]=merge(rs[a],rs[b],mid+1,r);
maxx[root]=max(maxx[ls[root]],maxx[rs[root]]);
res[root]=maxx[root]==maxx[ls[root]]?res[ls[root]]:res[rs[root]];
}
这种方法的优点就是支持在线,但是比较费空间
为了更好地理解线段树合并的过程以及为什么一种操作只能离线下来,这里我画了一张图(用鼠标画的,很丑勿喷
画的是线段树1先和线段树2合并再和线段树3合并,其中虚线表示实际上不存在的边(动态开点),蓝线是指两棵树的对应结点都存在直接合并的过程,而紫线是指两棵树只有一棵有对应的结点合并的过程,可以发现第2棵树中被紫线连接的点再后续和线段树3合并时,它将会连向线段树3的图中最下面的结点,这样如果我们去查询线段树2的信息就会查到第三棵树去
例题#
P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并
我们可以在每个结点都开一棵线段树,维护这个房子里每种救济粮的个数(权值线段树),那么再运用树上差分的知识,其实我们就只需要修改四个点,最后一个点的答案就是它所在子树的线段树合并之后的答案
主要说说线段树合并,我们可以边 dfs
边进行合并操作,就是把儿子结点的线段树合并到父亲结点,合并的时候,两棵树其实是同步的我们需要判断这两棵树是否都有这个结点,如果有一个没有就直接接到a上,如果两个都有就把b上维护的值合并到a上
code
#include <bits/stdc++.h>
using namespace std;
int read(){
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return x*f;
}
const int MAXN=1e5,N=1e5+10;
int ver[N<<1],tot,nxt[N<<1],head[N<<1],n,m;
int fa[N],dfn[N],dfstime,sz[N],top[N],son[N],dep[N],ans[N];
int rs[N*67],ls[N*67],maxx[N*67],res[N*67],rt[N];
void add(int x,int y){
ver[++tot]=y,nxt[tot]=head[x],head[x]=tot;
ver[++tot]=x,nxt[tot]=head[y],head[y]=tot;
}
void dfs1(int u,int father){
fa[u]=father;sz[u]=1;dep[u]=dep[father]+1;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa[u]) continue;
dfs1(v,u);
sz[u]+=sz[v];
if(sz[v]>sz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int anc){
top[u]=anc;
if(son[u]) dfs2(son[u],anc);
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(top[v]) continue;
dfs2(v,v);
}
}
int LCA(int x,int y){
while(top[x]^top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
void merge(int &a,int &b,int l,int r){//其实有返回值的写法可能更好一些
if(!a||!b){a+=b;return;}
if(l==r){maxx[a]+=maxx[b];res[a]=l;return;}
int mid=(l+r)>>1;
merge(ls[a],ls[b],l,mid);//整棵树合并,所以两边都要,类似build
merge(rs[a],rs[b],mid+1,r);
maxx[a]=max(maxx[ls[a]],maxx[rs[a]]);
res[a]=maxx[a]==maxx[ls[a]]?res[ls[a]]:res[rs[a]];//判断是继承了哪个儿子
}
void upd(int &k,int l,int r,int x,int val){//动态开点
if(!k) k=++tot;
if(l==r){maxx[k]+=val;res[k]=l;return;}
int mid=(l+r)>>1;
if(x<=mid) upd(ls[k],l,mid,x,val);
else upd(rs[k],mid+1,r,x,val);
maxx[k]=max(maxx[ls[k]],maxx[rs[k]]);
res[k]=maxx[k]==maxx[ls[k]]?res[ls[k]]:res[rs[k]];
}
void dfs(int u,int father){
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa[u]) continue;
dfs(v,u);
merge(rt[u],rt[v],1,MAXN);//把儿子节点都合并到父亲
}
if(maxx[rt[u]]) ans[u]=res[rt[u]];
}
int main(){
n=read();m=read();
for(int i=1;i<n;++i){
int x=read(),y=read();
add(x,y);
}
dfs1(1,0);
dfs2(1,1);
for(int i=1;i<=m;++i){
int x=read(),y=read(),z=read(),lca=LCA(x,y);
upd(rt[x],1,MAXN,z,1);
upd(rt[y],1,MAXN,z,1);
upd(rt[lca],1,MAXN,z,-1);
if(fa[lca]) upd(rt[fa[lca]],1,MAXN,z,-1);
}
dfs(1,0);
for(int i=1;i<=n;++i) printf("%d\n",ans[i]);
return 0;
}
分类:
A——算法学习笔记
, 数据结构——线段树合并
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!