线段树合并

线段树合并,就是将两棵线段树对应位置相加,得到一棵新的线段树。

由于实际应用中,通常要对很多棵线段树进行多次合并,所以和主席树类似地,我们使用动态开点线段树来实现。

线段树合并的过程很简单:

  1. 设当前需要合并的两棵树的根为 \(x,y\) ,如果有一个树为空,则返回另一棵树的树根。如果到了叶子,直接把两个点的信息加起来并返回。
  2. 递归合并 \(x,y\) 的左子树,做合并后的树根的左子树。
  3. 递归合并 \(x,y\) 的右子树,做合并后的树根的右子树。
  4. pushup维护信息,返回树根。
int merge(int x, int y, int l, int r){// [l,r]为线段树节点对应的区间
	if(!x || !y) return x^y;
	if(l==r)return tr[x].mx+=tr[y].mx, x;
	int mid=(l+r)>>1;
	tr[x].lc=merge(tr[x].lc, tr[y].lc, l, mid);// 省空间,直接用x作新根节点
	tr[x].rc=merge(tr[x].rc, tr[y].rc, mid+1, r);
	return pushup(x), x;
}

可以证明但我不会,将一堆大小分别为 \(T_1,T_2,...,T_n\) 的线段树,合并为大小为 \(T\) 的线段树,总时间复杂度为 \(O(\sum_{i=1}^n T_i-T)\)

可以发现,这段代码把 \(x,y\) 合并之后,用 \(x\) 作合并后的新根节点。这样合并完后, \(y\) 没有任何作用,会不会浪费线段树的空间呢?

确实。最坏情况下,当两棵线段树形态一样时,合并后肯定有一个线段树的空间没用了。所以为什么常见的线段树合并都不写节点回收呢?

这取决于线段树合并的应用场景。首先我们注意到,合并过程中没有新建节点,所以合完后,被占用的总空间依然是合并前的总节点数。

与平衡树不同,线段树合并的题,一般来说,不会出现把一堆线段树合并后,再大量开新节点的情况,所以一般没必要节点回收。

当然,如果这个题卡空间,那还是写个节点回收吧。回收方法同leafy_tree,开个垃圾篓放垃圾节点就行。


线段树合并最常见的用途是维护子树内信息,常见方法是每个树上的点开一个线段树,然后自底向上合并。与树上启发式合并的思路非常像,所以,线段树合并的题,许多都能用树上启发式合并去做。

当然,这两种方法也有各自更适合的应用场景。

线段树合并要每个点都开线段树,需要较多空间。树上启发式合并只需要足以维护一个树上节点的空间,但自身带一个 \(\log\)

如果每个点都开一个线段树不会爆,那么用线段树合并更快。

如果空间放不下那么多可爱的线段树,那就只能用树上启发式合并了。

P4556 【模板】线段树合并 / [Vani 有约会] 雨天的尾巴

链修改直接搞不太好做,考虑树上差分。

把一个链修改 \(x,y\) ,拆成 \(x\) 上放一个救济粮, \(y\) 上放一个救济粮, \(lca(x,y)\) 上撤掉一个救济粮, \(fa[lca(x,y)]\) 撤掉一个救济粮。

每个点开一个数组 \(cnt[i]\) 表示救济粮 \(i\) 的出现次数。然后,对于每个点 \(u\) ,将 \(u\) 子树中的所有 \(cnt\) 数组按位相加,合完后数组维护的便是每个点实际的救济粮。

按位相加太慢了,所以用权值线段树维护 \(cnt\) 数组,自底向上进行线段树合并,合完后查询最大值即可。

p4556
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define eb emplace_back
using namespace std;

const int N=2e5+10;
struct Node{int l,r,lc,rc,mx,id;}tr[N*35];
struct QUE{int x,y,k;}Q[N<<2];
vector<int> g[N];
int siz[N], son[N], d[N], fa[N], Top[N], ans[N], pts[N];
int n,m,tot,cur,mx,cntp;

void Dfs1(int u){
	siz[u]=1;
	for(int v:g[u]){
		if(v==fa[u]) continue;
		d[v]=d[u]+1; fa[v]=u;
		Dfs1(v);
		if(siz[v]>siz[son[u]]) son[u]=v;
		siz[u]+=siz[v];
	}
}
void Dfs2(int u, int tp){
	Top[u]=tp;
	if(son[u]) Dfs2(son[u],tp);
	for(int v:g[u]){
		if(v==fa[u] || v==son[u]) continue;
		Dfs2(v,v);
	}
}

inline int lca(int x, int y){
	while(Top[x]!=Top[y]){
		if(d[Top[x]]<d[Top[y]]) swap(x,y);
		x=fa[Top[x]];
	}
	if(d[x]>d[y]) swap(x,y);
	return x;
}

inline int New(int l, int r){
	int u=++tot;
	tr[u].l=l; tr[u].r=r;
	return u;
}
inline void pushup(int u){
	Node lt=tr[tr[u].lc], rt=tr[tr[u].rc];
	if(lt.mx>rt.mx) tr[u].mx=lt.mx, tr[u].id=lt.id;
	else if(lt.mx==rt.mx && rt.mx>0) tr[u].mx=rt.mx, tr[u].id=lt.id;
	else if(rt.mx>0) tr[u].mx=rt.mx, tr[u].id=rt.id;
	else tr[u].mx=tr[u].id=0;
}
void changly(int u, int x, int v){
	if(tr[u].l==tr[u].r){
		tr[u].mx+=v; tr[u].id=tr[u].l; return;// 记得更新id
	}
	int mid=(tr[u].l+tr[u].r)>>1;
	if(x<=mid){
		if(!tr[u].lc) tr[u].lc=New(tr[u].l, mid);
		changly(tr[u].lc, x, v);
	}
	if(x>mid){
		if(!tr[u].rc) tr[u].rc=New(mid+1, tr[u].r);
		changly(tr[u].rc, x, v);
	}
	pushup(u);
}
int merge(int x, int y, int l, int r){// [l,r]为线段树节点对应的区间
	if(!x || !y) return x^y;
	if(l==r)return tr[x].mx+=tr[y].mx, x;
	int mid=(l+r)>>1;
	tr[x].lc=merge(tr[x].lc, tr[y].lc, l, mid);// 省空间,直接用x作新根节点
	tr[x].rc=merge(tr[x].rc, tr[y].rc, mid+1, r);
	return pushup(x), x;
}

int Dfs3(int u){
	int x=u;
	for(int v:g[u]){
		if(v==fa[u]) continue;
		int y=Dfs3(v);
		x=merge(x, y, 1, mx);
	}
	ans[u]=tr[x].id;
	return x;
}

signed main(){
	cin.tie(0)->sync_with_stdio(0);
	cin>>n>>m;
	for(int i=1; i<n; i++){
		int u,v; cin>>u>>v;
		g[u].eb(v); g[v].eb(u);
	}
	Dfs1(1); Dfs2(1,1);
	for(int i=1; i<=m; i++){
		int x,y,z; cin>>x>>y>>z;
		int l=lca(x,y);
		Q[++cur]={x,z,1}; Q[++cur]={y,z,1};
		Q[++cur]={l,z,-1};
		if(fa[l]) Q[++cur]={fa[l],z,-1};
		mx=max(mx,z);
	}
	for(int i=1; i<=n; i++) New(1,mx);
	for(int i=1; i<=cur; i++)
		changly(Q[i].x, Q[i].y, Q[i].k);
	Dfs3(1);
	for(int i=1; i<=n; i++) cout<<ans[i]<<"\n";
	return 0;
}

当然,这个题也可以用树上启发式合并+multiset做,不过会多一个 \(\log\)

posted @ 2026-04-04 16:43  Cute_lxy  阅读(4)  评论(0)    收藏  举报