线段树合并

适用场景

• 树上问题:在树上需要合并子树信息时,可以使用线段树合并来优化时间复杂度。
• 动态开点:通常与动态开点的线段树结合使用,以减少空间消耗。

算法流程

  1. 递归合并:从根节点开始,同时遍历两棵线段树。
  2. 空节点处理:如果某个位置的节点在一棵树中为空,则直接使用另一棵树中的节点。
  3. 叶子节点处理:到达叶子节点时,直接合并该节点的信息。
  4. 非叶子节点处理:对于非叶子节点,递归合并左右子树,并更新当前节点的信息。

时间复杂度

• 线段树合并的时间复杂度为 O(nlogn),其中 n是有效节点的总数。这是因为每次合并操作只会遍历两棵树共有的节点,避免了不必要的递归。

实现技巧

• 标记永久化:在某些情况下,可以使用标记永久化来简化区间修改的操作。
• 动态开点:只在需要时创建节点,可以显著减少空间消耗。

应用示例

• 树上差分:结合线段树合并,可以在树上进行区间修改和查询操作。(转区间操作为单点操作,适用于树上每个节点都要维护一个线段树的题目)
• 集合合并:维护每个集合的权值线段树,通过合并线段树来合并集合。

例题

1.洛谷P4556 (树上差分+线段树合并)

2.洛谷P1600 (也是树上差分+线段树合并,有一定思维难度)

洛谷P1600题解

1.处理好每个人跑步过程的lca和dis

2.不能从每个人每一步会经过哪些观察员这个视角去做题,要从每个人过程中会给什么情况的观察员做贡献去考虑

3.我们记运动员从S到T,观察员位于j点,i点在树上的深度为dep[i]
通过推到得到,对于S到lca的路径,若观察员满足dep[S]-dep[j]=w[j],也就是dep[S]=dep[j]+w[j],那么该运动员会对其产生1的贡献
对于lca到T的过程,若观察员满足dep[t]-dep[j]=dis-w[j],也就是dep[t]-dis=dep[j]-w[j],那么该运动员会对其产生1的贡献

4.那这样我们就可以通过树上差分转化路径加1为单点加1,然后发现对于每个观察员,所能给他做贡献的S和T都在其子树内部(很合理,因为无论是S,还是T到lca都是一个向上走的过程)(要保证不会计算重复,我们对于两种贡献,不能把lca点处理两遍),所以就可以通过单点修改到最后进行线段树合并得到每个点的值了

#include<bits/stdc++.h>
#define pb push_back
#define mid (l+(r-l)/2)
#define ls(op,p) d[op][p].ls
#define rs(op,p) d[op][p].rs
using namespace std;
const int N=300010;
int n,m;
vector<int> g[N];
int w[N];
int ans[N];
struct PER{
	int s,t;
}per[N];
int dep[N],size[N],son[N],fa[N];
int top[N];
void dfs1(int s,int f){
	fa[s]=f; size[s]=1; dep[s]=dep[f]+1;
	for(int to:g[s]){
		if(to==f) continue;
		dfs1(to,s);
		if(size[son[s]]<size[to]) son[s]=to;
		size[s]+=size[to];
	}
}
void dfs2(int s,int Top){
	top[s]=Top;
	if(son[s]) dfs2(son[s],Top);
	for(int to:g[s]){
		if(to==fa[s]||to==son[s]) continue;
		dfs2(to,to);
	}
}
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 y;
}
void init(){
	dfs1(1,0);
	dfs2(1,1);
}
struct node{
	int ls,rs;
	int cnt;
}d[2][30*N];
int root[2][N],tot;
void up(int op,int p){
	d[op][p].cnt=d[op][ls(op,p)].cnt+d[op][rs(op,p)].cnt;
}
void change(int &p,int l,int r,int pos,int num,int op){//单点修改 
	if(!p) p=++tot;
	if(l==r){
		d[op][p].cnt+=num;
		return;
	}
	if(pos<=mid) change(ls(op,p),l,mid,pos,num,op);
	else change(rs(op,p),mid+1,r,pos,num,op);
	up(op,p);
}
int query(int p,int l,int r,int pos,int op){
	if(pos>r||!p) return 0;
	if(l==r) return d[op][p].cnt;
	if(pos<=mid) return query(ls(op,p),l,mid,pos,op);
	else return query(rs(op,p),mid+1,r,pos,op);
}
int merge(int x,int y,int l,int r,int op){//线段树合并,并且取值 
	if(!x||!y) return x+y;
	if(l==r){
		d[op][x].cnt+=d[op][y].cnt;
		return x;
	}
	ls(op,x)=merge(ls(op,x),ls(op,y),l,mid,op);
	rs(op,x)=merge(rs(op,x),rs(op,y),mid+1,r,op);
	up(op,x);
	return x;
} 
void calc(int s,int f){
	for(int to:g[s]){
		if(to==f) continue;
		calc(to,s); 
		root[0][s]=merge(root[0][s],root[0][to],1,n,0);
		root[1][s]=merge(root[1][s],root[1][to],1,2*n,1);
	}
	ans[s]=query(root[0][s],1,n,dep[s]+w[s],0)+query(root[1][s],1,2*n,dep[s]-w[s]+n,1);
}
void solve(){
	for(int i=1;i<=m;i++){
		int lca=LCA(per[i].s,per[i].t);
		int dis=dep[per[i].s]+dep[per[i].t]-2*dep[lca];
		//对s到lca的贡献进行统计 
		int pos1=dep[per[i].s];//将要单点修改的值域点 
		//树上差分 
		change(root[0][per[i].s],1,n,pos1,1,0);
		change(root[0][fa[lca]],1,n,pos1,-1,0);
		
		//对lca到s的贡献进行统计 
		int pos2=dep[per[i].t]-dis+n;
		change(root[1][per[i].t],1,2*n,pos2,1,1);
		change(root[1][lca],1,2*n,pos2,-1,1);
	}
	calc(1,0); //对差分进行前缀和复原 
}
int main(){
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	ios::sync_with_stdio(0); 
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		g[u].pb(v);
		g[v].pb(u);
	}
	for(int i=1;i<=n;i++) cin>>w[i];
	init();
	for(int i=1;i<=m;i++) cin>>per[i].s>>per[i].t;
	solve();
	for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
	return 0;
}

完结

posted @ 2025-07-16 08:36  hapihapi  阅读(168)  评论(0)    收藏  举报