关于树剖

wenge

2021-04-07 20:10:21

Personal

## 树剖是啥? ### 树剖就是分块——slh 广义的树剖指把一个树拆成若干条链。方法有多种,比如重链剖分,长链剖分等 狭义的树剖专指重链剖分。接下来只讨论最常用的重链剖分。 定义一个有根树上,一个结点子树大小最大的儿子为**重儿子**。其余的为轻儿子。如果多个儿子子树大小相同,则任选一个。如果没有儿子,就没有重儿子。 定义从一个点连向其重儿子的边叫**重边**,连向其余儿子的叫**轻边**。 定义由若干点和重边组成的一条链为**重链**。特别的,落单的一个节点也是重链。定义一条重链中深度最小的点为这条重链的**顶**。 ![](https://oi-wiki.org/graph/images/hld.png) ## 树剖有啥性质? 重链剖分将树分割成不超过$O(\log n)$条重链。 每条重链、每个子树内dfs序连续。(对于每个点,优先dfs其重儿子的前提下) ## 树剖怎么做? 2次dfs维护7个数组。~~召唤神龙~~ dfs1()对于一个点维护其 父亲fa[],深度dep[],子树大小size[],重儿子hson[] dfs2()对于一个点维护其 所在重链的顶top[],dfs序dfn[],满足rnk[dfn[x]]=x的值(有时用不到)rnk[] 细节参见代码。 std: ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; #define $ 500005 ll n,r;//r为根 ll fa[$],dep[$],size[$],hson[$]; ll top[$],dfn[$],rnk[$]; ll tot=0; void dfs1(ll x,ll z){//x是当前节点,z是父亲 fa[x]=z;//求出父亲 dep[x]=dep[z]+1;//求出深度 size[x]=1;//当前节点的大小为1 ll t=0; for(int i=0;i<f[x].size();i++){ ll y=f[x][i]; if(y==z)continue; dfs1(y,x); size[x]+=size[y];//统计子树的大小 if(size[y]>t){ t=size[y]; hson[x]=y;//更新重儿子 } } } void dfs2(ll x,ll t){//x是当前节点,z是x所在重链的顶 top[x]=t;//求出链顶 dfn[x]=++tot;//求出dfn rnk[tot]=x; if(hson[x]!=0){ dfs2(hson[x],t);//必须先dfs重儿子才能保证dfn的连续性质 for(int i=0;i<f[x].size();i++){//dfs轻儿子 ll y=f[x][i]; if(y==hson[x]||y==fa[x])continue; dfs2(y,y); } } } ``` ``` int main(){//调用的方法 //...... dfs1(r,0); dfs2(r,r); //...... return 0; } ``` ## 树剖有啥用? ~~切题~~ ### 1.求lca。 方法是:重复深度较深的点跳到他链顶的父亲(向上跳链),直到跳到同一链上。此时深度较小的点即为lca。 我之前打洛谷lca板子用倍增+vector被卡常了。树剖+vector屹立不倒。挺快。 实际应用还要考虑哪个维护题目中所使用的附加信息方便。 std: ```cpp //#pragma GCC optimize(2) #include <bits/stdc++.h> using namespace std; typedef long long ll; #define pb push_back #define clr(a,b) memset(a,b,sizeof(a)) #define $ 500005 ll n,p,r; ll a[$]; vector<ll> f[$]; ll fa[$],dep[$],size[$],hson[$]; ll top[$],dfn[$],rnk[$]; ll tot=0; void dfs1(ll x,ll z){ fa[x]=z; dep[x]=dep[z]+1; size[x]=1; ll t=0; for(int i=0;i<f[x].size();i++){ ll y=f[x][i]; if(y==z)continue; dfs1(y,x); size[x]+=size[y]; if(size[y]>t){ t=size[y]; hson[x]=y; } } } void dfs2(ll x,ll t){ top[x]=t; dfn[x]=++tot; rnk[tot]=x; if(hson[x]!=0){ dfs2(hson[x],t); for(int i=0;i<f[x].size();i++){ ll y=f[x][i]; if(y==hson[x]||y==fa[x])continue; dfs2(y,y); } } } ll lca(ll x,ll y){ while(top[x]!=top[y]){ if(dep[top[x]]>=dep[top[y]])x=fa[top[x]]; else y=fa[top[y]]; } if(dep[x]<=dep[y])return x; return y; } int main(){ ios::sync_with_stdio(false); cin.tie(0); ll q; cin>>n>>q>>r; //for(int i=1;i<=n;i++)cin>>a[i]; for(int i=1;i<n;i++){ ll u,v; cin>>u>>v; f[u].pb(v);f[v].pb(u); } dfs1(r,0); dfs2(r,r); for(int i=1;i<=q;i++){ ll u,v; cin>>u>>v; cout<<lca(u,v)<<"\n"; } return 0; } ``` ### 2.维护树上信息 能够配合线段树、树状数组等数据结构维护树上信息。 例子:给你一棵树,点带权,4个操作: 1. 点x到点y的路径上点权增加a 1. 查询点x到点y的路径上点权之和 1. 以x为根的子树所有点权增加a 1. 询以x为根的子树所有点权之和 先看操作4。上面的性质告诉我们一个子树的dfs序连续。 对于点权建立线段树(或者树状数组),其中线段树第i个值是dfn为i的点的点权。 由于整个子树在线段树里都是一个连续的区间,并且维护了子树大小size[x]。所以直接对区间[dfn[x],dfn[x]+size[x]-1]进行区间求和即可。操作3同理,只需把区间求和改成区间加。复杂度$O(\log n)$。 再看操作2。操作2中的路径可以分成若干连续部分,每一部分在一条重链上。于是对这些部分分别区间求和即可。求和实现类似于上面lca的求法。具体见代码。操作1同理。由于整棵树不超过$O(\log n)$条重链,所以至多拆成$O(\log n)$部分,每次求和$O(\log n)$。所以复杂度$O(\log^2 n)$。于是就做完了。 同样可以解决路径赋值、子树赋值等问题。 ```cpp //#pragma GCC optimize(2) #include <bits/stdc++.h> using namespace std; typedef long long ll; #define pb push_back #define clr(a,b) memset(a,b,sizeof(a)) #define $ 100005 ll n,slh,r; ll a[$],b[$]; vector<ll> f[$]; ll fa[$],dep[$],size[$],hson[$]; ll top[$],dfn[$],rnk[$]; ll tot=0; void dfs1(ll x,ll z){ fa[x]=z; dep[x]=dep[z]+1; size[x]=1; ll t=0; for(int i=0;i<f[x].size();i++){ ll y=f[x][i]; if(y==z)continue; dfs1(y,x); size[x]+=size[y]; if(size[y]>t){ t=size[y]; hson[x]=y; } } } void dfs2(ll x,ll t){ top[x]=t; dfn[x]=++tot; a[tot]=b[x];//点权在此处整理,准备好建立线段树 rnk[tot]=x; if(hson[x]!=0){ dfs2(hson[x],t); for(int i=0;i<f[x].size();i++){ ll y=f[x][i]; if(y==hson[x]||y==fa[x])continue; dfs2(y,y); } } } struct node{ ll x,add; }sgt[4*$]; void pushup(ll i){ sgt[i].x=(sgt[i*2].x+sgt[i*2+1].x)%slh; } void pushdown(ll mid,ll l,ll r,ll i){ if(sgt[i].add){ sgt[i*2].x=(sgt[i*2].x+(mid-l+1)*sgt[i].add)%slh; sgt[i*2+1].x=(sgt[i*2+1].x+(r-mid)*sgt[i].add)%slh; sgt[i*2].add=(sgt[i*2].add+sgt[i].add)%slh; sgt[i*2+1].add=(sgt[i*2+1].add+sgt[i].add)%slh; sgt[i].add=0; } } void build(ll l,ll r,ll i){ if(l==r){ sgt[i].x=a[l]%slh; return; } ll mid=(l+r)>>1; build(l,mid,i*2); build(mid+1,r,i*2+1); pushup(i); } ll query(ll s,ll t,ll l,ll r,ll i){ if(s<=l&&r<=t){ return sgt[i].x; } ll mid=(l+r)>>1; pushdown(mid,l,r,i); ll res=0; if(s<=mid)res+=query(s,t,l,mid,i*2); if(t>mid)res+=query(s,t,mid+1,r,i*2+1); pushup(i); return res%slh; } void modify(ll s,ll t,ll l,ll r,ll i,ll x){ if(s<=l&&r<=t){ sgt[i].x+=(r-l+1)*x; sgt[i].add+=x; return; } ll mid=(l+r)>>1; pushdown(mid,l,r,i); if(s<=mid)modify(s,t,l,mid,i*2,x); if(t>mid)modify(s,t,mid+1,r,i*2+1,x); pushup(i); } void op1(ll x,ll y,ll z){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); modify(dfn[top[x]],dfn[x],1,n,1,z); x=fa[top[x]]; } if(dfn[x]>dfn[y])swap(x,y); modify(dfn[x],dfn[y],1,n,1,z); } ll op2(ll x,ll y){ ll res=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); res=(res+query(dfn[top[x]],dfn[x],1,n,1))%slh; x=fa[top[x]]; } if(dfn[x]>dfn[y])swap(x,y); res=(res+query(dfn[x],dfn[y],1,n,1))%slh; return res; } void op3(ll x,ll z){ modify(dfn[x],dfn[x]+size[x]-1,1,n,1,z); } ll op4(ll x){ return query(dfn[x],dfn[x]+size[x]-1,1,n,1); } int main(){ //freopen("P1429_6.in","r",stdin); //freopen("match.out","w",stdout); ios::sync_with_stdio(false); cin.tie(0); ll q; cin>>n>>q>>r>>slh; for(int i=1;i<=n;i++)cin>>b[i]; for(int i=1;i<n;i++){ ll u,v; cin>>u>>v; f[u].pb(v);f[v].pb(u); } dfs1(r,0); dfs2(r,r); build(1,n,1); while(q--){ ll op; cin>>op; if(op==1){ ll x,y,z; cin>>x>>y>>z; op1(x,y,z); } if(op==2){ ll x,y; cin>>x>>y; cout<<op2(x,y)<<"\n"; } if(op==3){ ll x,z; cin>>x>>z; op3(x,z); } if(op==4){ ll x; cin>>x; cout<<op4(x)<<"\n"; } } return 0; } ```