线段树合并(随笔)

线段树合并

来来来一百种线段树,线段树怎么这么博大精深啊

今天来讲一下线段树合并:

线段树合并意义:

顾名思义,我们现在有两棵线段树,我们合并他们,就是建一棵新的线段树来包含这两棵线段树的信息,不难发现,对于普通线段树,在同等规模下,两棵线段树的结构是相同的,这时候直接按位进行合并就行了,不需要设计算法来合并,所以这个算法是针对动态开点这种残缺不全的线段树来操作的

与之而来的有几个问题:

1.如何建立这一棵新的树?
2.对于两棵线段树的重合/空缺位置我们该这么处理?

假设合并要进行的是加操作,那么我们就把线段树 1 的值加入线段树 2 中构成新的线段树,那么如何实现呢?
我们同时在两颗线段树上进行 \(dfs\),进入一个节点时判断是否两棵树的该节点都有左儿子,如果都有就进去继续 \(dfs\),反之,若是树 1 没有左儿子或1,2树都没有,则无视此操作,若是树 2 没有左儿子,那直接把树 1 的左儿子赋给树 2 就行了。要是都没有左右节点,直接把 1 赋给 2 然后返回就行了,对于一个节点的 \(dfs\) 结束后,向上传递信息。

不好具体来阐述所以我们来看一道例题P4556 雨天的尾巴 /【模板】线段树合并 - 洛谷

乍一看有树上路径,很像树链剖分,事实上确实可以用树剖写,但是却是线段树合并模板?

暴力处理我们将\(a,b\)到根的所有节点加上权值,再让\(lca(a,b)\)\(lca(a,b)\)的父亲到根的所有节点减去权值,若直接遍历时间复杂度是可想而知的大,所以我们可以进行树上差分,我们来看看如何用线段树优化,首先我们要进行操作的是权值线段树,对每一个节点都建一棵权值线段树,对应整个救济粮的值域区间,维护每种救济粮有多少,记为\(sum\),以及最多的救济粮的个数,记为 \(maxk\)

所以一次操作对应
$sum[x]++,sum[y]++,sum[x]++,sum[y]++ $

$ sum[lca(x,y)]−−,sum[fa[lca(x,y)]]−−$
仅在 4 个点上统计变化情况

在最后统计答案的时候,就用到了线段树合并,在每个节点线段树上将原先差分的结果加起来就是答案,具体实现见代码

果然还是树剖好理解

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=80*N;
struct Edge{
	int to;
	int next;
}e[N<<1];
int h[N];
int cnt;
int n,m;
struct SG{
	int lson;
	int rson;
	int sum;
	int maxk;
	int l,r;
}tr[M];
int ans[N];
int root[N];
int f[N][21];
int dep[N];
void add(int a,int b){
	e[cnt].to=b;
	e[cnt].next=h[a];
	h[a]=cnt++;
	return ;
}
void pushup(int u){
	int l=tr[u].lson;
	int r=tr[u].rson;
	if(tr[l].sum>=tr[r].sum){
		tr[u].sum=tr[l].sum;
		tr[u].maxk=tr[l].maxk;
	}
	else{
		tr[u].sum=tr[r].sum;
		tr[u].maxk=tr[r].maxk;
	}
	//合并求区间众数,具体为什么不用更新sum的原因是
	//在modify中  l==r 时这单个数的贡献已经统计完了
	//后面区间变大时不会出现使这个数贡献增加的情况 
}
void modify(int &u,int l,int r,int z,int val){
	if(!u) u=++cnt;
	if(l==r){
		tr[u].sum+=val;
		tr[u].maxk=z;
		return ;
	}
	int mid=l+r>>1;
	if(z<=mid) modify(tr[u].lson,l,mid,z,val);
	else modify(tr[u].rson,mid+1,r,z,val);
	pushup(u);
}
int merge(int u,int v,int l,int r){
	if(!u||!v) return u+v;//左/右子节点没有直接加然后返回不会错 
	if(l==r){
		tr[u].sum+=tr[v].sum;//叶节点加权然后返回 
		return u;
	}
	int mid=l+r>>1;
	tr[u].lson=merge(tr[u].lson,tr[v].lson,l,mid);// dfs下去 ,用线段树的遍历方式 
	tr[u].rson=merge(tr[u].rson,tr[v].rson,mid+1,r);
	pushup(u);//合并答案 
	return u;
}
void calc(int u,int fa){
	for(int i=h[u];~i;i=e[i].next){
		int v=e[i].to;
		if(v==fa) continue;
		calc(v,u);//注意是先遍历,从子节点到父节点,这是显而易见的 
		root[u]=merge(root[u],root[v],1,1e5); //从头到脚合并一遍 
	} 
	ans[u]=tr[root[u]].maxk;
	if(!tr[root[u]].sum){
		ans[u]=0;
	}
}
void dfs(int u,int fa){
	f[u][0]=fa;
	dep[u]=dep[fa]+1;
	for(int i=1;i<20;i++){
		f[u][i]=f[f[u][i-1]][i-1];//预处理,倍增跳LCA 
	}
	for(int i=h[u];~i;i=e[i].next){
		int v=e[i].to;
		if(v==fa){
			continue;			
		}
		dfs(v,u);
	} 
}
int lca(int a,int b){
	if(dep[a]<dep[b]){
		swap(a,b);
	}
	for(int i=19;i>=0;i--){
		if(dep[f[a][i]]>=dep[b]){
			a=f[a][i];
		}
	}
	if(a==b)  return a;
	for(int i=19;i>=0;i--){
		if(f[a][i]!=f[b][i]){
			a=f[a][i];
			b=f[b][i];
		}
	}
	return f[a][0];
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);    cout.tie(0);
	memset(h,-1,sizeof h);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs(1,0);
	while(m--){
		int x,y,z;
		cin>>x>>y>>z;
		int k=lca(x,y);
		modify(root[x],1,1e5,z,1);
		modify(root[y],1,1e5,z,1);
		modify(root[k],1,1e5,z,-1);
		modify(root[f[k][0]],1,1e5,z,-1);
	}
	calc(1,0);
	for(int i=1;i<=n;i++){
		cout<<ans[i]<<endl;;
	}
	return 0;
} 
posted @ 2025-05-22 09:31  Zom_j  阅读(41)  评论(0)    收藏  举报