线段树合并
线段树合并,就是将两棵线段树对应位置相加,得到一棵新的线段树。
由于实际应用中,通常要对很多棵线段树进行多次合并,所以和主席树类似地,我们使用动态开点线段树来实现。
线段树合并的过程很简单:
- 设当前需要合并的两棵树的根为 \(x,y\) ,如果有一个树为空,则返回另一棵树的树根。如果到了叶子,直接把两个点的信息加起来并返回。
- 递归合并 \(x,y\) 的左子树,做合并后的树根的左子树。
- 递归合并 \(x,y\) 的右子树,做合并后的树根的右子树。
- 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\) 。
浙公网安备 33010602011771号