线段树合并常用于树上的问题,假如每个点有一个权值线段树存储信息,那么如果我们想知道一棵子树的总信息,就需要把子树中所有点的线段树都合并到子树根结点的那个线段树那里。
怎么合成呢?假如是对于两个满的线段树,直接从底层开始(假设底层信息分别记录在a1[N]和a2[N]中)让a1[i] += a2[i],然后重新生成一棵树。这是最暴力的方法,复杂度是不ok的。
但是实际上线段树往往是不满的,所以我们只要对线段树动态开点,就能实现更优的合并。最终复杂度是O(nlogn),证明略。
例题:[Vani有约会]雨天的尾巴 /【模板】线段树合并 - 洛谷
这题要用到树上差分和线段树合并。
1.树上差分体现在:x~y部分,z种类粮食数量+1,等效于1~x的z种类+1, 1~y的z种类+1, 1~lca的z种类-1, 1~fa[lca]的z种类-1。这四部分合在一起形成树上差分。
int u,v,w; cin>>u>>v>>w;
int l = LCA(u,v); //l是u和v的公共祖先
upd(rt[u], 1, 1e5, w, 1); //维护四个差分,从而形成u~v部分w数量+1的效果
upd(rt[v], 1, 1e5, w, 1);
upd(rt[l], 1, 1e5, w, -1);
upd(rt[f[l][0]], 1, 1e5, w, -1);
2.线段树合并体现在:要把差分转化成值的时候,要计算子树的合并线段树。
举个例子,1有两个孩子,2和3,那么要知道1的信息,就要把2和3的线段树合成到1上。
code:
补充一点需要注意的:线段树空间必须开够,要4*n*logn的空间(每次发粮有4个upd)
#include<bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
const int N = 1e5+5;
int n,m,ans[N],f[N][30],lg[N],d[N];
int rt[N], ls[N*80], rs[N*80], cnt=0; //根结点,左右孩子,当前编号
pair<int,int> t[N*80]; //树的本体,{救济粮数量,种类编号}
vector<int> g[N];
void dfs(int u,int fa){
f[u][0] = fa, d[u] = d[fa]+1;
FOR(i,1,lg[d[u]])
f[u][i] = f[f[u][i-1]][i-1];
for(int v:g[u])
if(v!=fa) dfs(v,u);
}
int LCA(int x,int y){
if(d[x] < d[y]) swap(x,y);
while(d[x] > d[y]) x = f[x][lg[d[x]-d[y]]];
if(x==y) return x;
for(int k=lg[d[x]]; k>=0; k--)
if(f[x][k] != f[y][k]) {x=f[x][k]; y=f[y][k];}
return f[x][0];
}
void pushup(int o){
//同一棵线段树上的操作,某个节点收集自己两个儿子的信息,得到这个区间的{最大数量,对应粮食种类}
t[o] = max(t[ls[o]], t[rs[o]]);
}
void upd(int&o,int l,int r,int pos,int val){
//给第o棵树的pos位置+val
if(!o) o = ++cnt; //如果没有结点,新建一个
if(l==r){
t[o].first += val; //pos种类的粮数量+val
t[o].second = -pos; //标记这里的粮食种类是pos(这里标记负是为了方便操作)
return;
}
int mid = l+r>>1;
if(pos<=mid) upd(ls[o],l,mid,pos,val);
else upd(rs[o],mid+1,r,pos,val);
pushup(o);
}
int merge(int o,int p,int l,int r){
//把p树合成到o树上,返回新树的根结点编号
if(o==0 || p==0) return o+p; //有一棵树空的,直接用另一棵树替代
if(l==r) {t[o].first += t[p].first; return o;} //都不是空的,并且到叶子节点了,把p合成到o上
int mid = l+r>>1;
ls[o] = merge(ls[o],ls[p],l,mid); //左半边和右半边递归合成
rs[o] = merge(rs[o],rs[p],mid+1,r);
pushup(o); //儿子节点成功合成另一棵树信息之后,重新维护父亲的信息
return o; //别忘了返回原结点
}
void calc(int u,int fa){
for(int v:g[u]){
if(v==fa) continue;
calc(v,u);
rt[u] = merge(rt[u],rt[v],1,1e5); //线段树合并的核心步骤!把rt[v]这棵树合成到rt[u]上去,合成范围是[1,1e5]
}
if(t[rt[u]].first!=0) ans[u] = -t[rt[u]].second;
}
inline void solve(){
cin>>n>>m;
FOR(i,1,n-1){
int u,v; cin>>u>>v;
g[u].push_back(v), g[v].push_back(u);
}
FOR(i,2,n) lg[i]=lg[i>>1]+1; //预处理log2(i)
dfs(1,0); //预处理出倍增跳找LCA的信息
FOR(i,1,m){
int u,v,w; cin>>u>>v>>w;
int l = LCA(u,v); //l是u和v的公共祖先
upd(rt[u], 1, 1e5, w, 1); //维护四个差分,从而形成u~v部分w数量+1的效果
upd(rt[v], 1, 1e5, w, 1);
upd(rt[l], 1, 1e5, w, -1);
upd(rt[f[l][0]], 1, 1e5, w, -1);
}
calc(1,0); //类似树形dp,进行线段树合并(差分->值)
FOR(i,1,n) cout<<ans[i]<<'\n';
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T=1; while(T--) solve();
}