樹鏈剖分
1. 相關概念
-
重兒子:父親節點的所有兒子中子樹結點數目最多(
size
最大)的結點; -
輕兒子:父親節點中除了重兒子以外的兒子;
-
重邊:父親結點和重兒子連成的邊;
-
輕邊:父親節點和輕兒子連成的邊;
-
重鏈:由多條重邊連接而成的路徑;
-
輕鏈:由多條輕邊連接而成的路徑
-
如下圖所示
-
2. 樹鏈剖分的實現
-
- 上圖,紅點為重鏈的起點,加粗黑邊為重邊,細邊為輕邊,加粗黑圈為重子節點,其他為輕子節點,節點右邊的數字為第二遍
bfs
遍歷順序。
- 上圖,紅點為重鏈的起點,加粗黑邊為重邊,細邊為輕邊,加粗黑圈為重子節點,其他為輕子節點,節點右邊的數字為第二遍
-
首先求出每個節點所在的子樹大小,找到它的重兒子(即預處理出
size,son
數組)- 比如:節點
1
的三個子節點,size[2]=5,size[3]=2,size[4]=6
,節點最大的是4
,所以,節點1
的重兒子是節點4
。 - 如果一個節點的多個子節點一樣大,且均為最大節點,那隨便找一個當做它的重兒子。
- 葉節點沒有重兒子,非葉節點有且只有一個重兒子。
- 比如:節點
-
在
dfs
過程中順便記錄其父親以及深度,操作1,2
可以通過一遍dfs
完成。void dfs1(int u,int fa){ //預處理出當前節點、父節點、層次深度 f[u]=fa;size[u]=1; //這個點本身size=1 for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(v==fa)continue; deep[v]=deep[u]+1; dfs1(v,u); //層次深度+1 size[u]+=size[v]; //子節點的size已被處理,用它來更新父節點的size if(size[v]>size[son[u]])//選取size最大的作為重兒子 son[u]=v; //son[u]表示u的重兒子 } }
-
第二遍
dfs
,連接重鏈,同時標記每一個節點的dfs
序,並且為了用數據結構來維護重鏈,我們在dfs
時保證一條重鏈上各個節點dfs
序連續。void dfs2(int u,int t){ //當前節點、重鏈頂端 top[u]=t;//保存當前節點所在鏈的頂端節點 dfn[u]=++cnt; //cnt標記dfs序 rk[cnt]=u; //序號cnt對應節點u if(son[u])dfs2(son[u],t);//先走重兒子 /*我們選擇優先進入重兒子來保證一條重鏈上各個節點dfs序連續, 一個點和它的重兒子處於同一條重鏈,所以重兒子所在重鏈的頂端還是t*/ for(int i=head[u];i;i=e[i].next){//遍歷輕鏈 int v=e[i].to; if(v!=son[u]&&v!=f[u]) dfs2(v,v); //一個點位於輕鏈底端,那么它的top必然是它本身 } }
3. 樹鏈刨分LCA
-
算法實現:求樹上節點
u,v
的LCA
。- 如果
u,v
在同一個重鏈上,即,top[u]==top[v]
,則深度小的為LCA
。 - 節點
u,v
不在同一個重鏈,讓深度大的鏈頂節點u
往上跳,跳到其鏈頂的父親節點上,即u=f[top[u]]
. - 重復步驟
2
直到節點u,v
在同一個重鏈,此時深度小的為LCA
。
- 如果
-
例題:
luogu P3379
樹鏈剖分求LCA -
Code
#include <bits/stdc++.h> const int maxn=5e5+5; struct edge{ int to,next; }e[2*maxn]; int n,m,root,len,head[maxn],deep[maxn],siz[maxn],son[maxn],top[maxn],f[maxn]; void Insert(int x,int y){ e[++len].to=y;e[len].next=head[x];head[x]=len; } void dfs1(int u){ siz[u]=1;deep[u]=deep[f[u]]+1; for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(v==f[u])continue; f[v]=u; dfs1(v); siz[u]+=siz[v]; if(!son[u]||siz[son[u]]<siz[v])//求重兒子 son[u]=v; } } void dfs2(int u,int tp){ top[u]=tp; if(son[u])dfs2(son[u],tp); for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(v!=f[u] && v!=son[u])//v是輕兒子 dfs2(v,v); } } int LCA(int u,int v){ while(top[u]!=top[v]){ if(deep[top[u]]>=deep[top[v]]) u=f[top[u]]; else v=f[top[v]]; } return deep[u] < deep[v] ? u : v; } void Solve(){ scanf("%d%d%d",&n,&m,&root); for(int i=1;i<n;++i){ int x,y;scanf("%d%d",&x,&y); Insert(x,y);Insert(y,x); } dfs1(root); dfs2(root,root); for(int i=1;i<=m;++i){ int u,v;scanf("%d%d",&u,&v); printf("%d\n",LCA(u,v)); } } int main(){ Solve(); return 0; }
-
時間效率:
dfs
為O(n)
,查詢一次為:O(log(n))
,總的時間效率:O(m*log(n))
。 -
當
u,v
均在輕鏈的底端是,每次往上跳時只能從父親節點一個個往上跳,但輕鏈到根節點距離小於log(n)
,一般情況下樹鏈刨分的常數非常小,不到1/2
。
樹鏈剖分(線段樹)
-
例題:
luogu P3384
【模板】輕重鏈剖分 -
分析:
-
區間修改、區間查詢是線段樹的基本操作,但我們熟悉的是線性數據結果上的操作。
-
樹鏈剖分正好能把樹上的路徑划分成一條條重鏈,一條重鏈正好是一個區間。
-
Code
#include<bits/stdc++.h> using namespace std; const int maxn=1e5+1000; struct edge{int next,to;}e[maxn<<2]; struct node{int l,r,w,siz,lazy;}tr[maxn<<2]; int len,root,MOD,cnt=0,n,m; int a[maxn],head[maxn];; int deep[maxn],f[maxn],son[maxn],size[maxn],top[maxn],dnf[maxn],rk[maxn]; void Insert(int u,int v){ e[++len].to=v;e[len].next=head[u];head[u]=len; } void dfs1(int u,int fa){//處理深度,父親節點,u為根的節點個數 size[u]=1; for(int i=head[u];~i;i=e[i].next){ int v=e[i].to; if(v==fa) continue; deep[v]=deep[u]+1;f[v]=u; dfs1(v,u); size[u]+=size[v]; if(!son[u] || size[v]>size[son[u]])//重鏈的兒子 son[u]=v; } } void dfs2(int u,int tp){ top[u]=tp;//標記重鏈的頂點 dnf[u]= ++cnt;//節點對應編號 rk[cnt]=a[u];//編號對應節點建樹的關鍵一員 if(son[u]) dfs2(son[u],tp); for(int i=head[u];~i;i=e[i].next){ int v=e[i].to; if(v!=son[u] && v!=f[u]) dfs2(v,v);//非重鏈的頂點就是自己 } } void push_up(int u){ tr[u].w=(tr[u<<1].w+tr[u<<1|1].w+MOD)%MOD; } void build(int u,int l,int r){ tr[u].l=l;tr[u].r=r;tr[u].siz=r-l+1; if(l==r){ tr[u].w=rk[l]; return ; } int mid=(l+r)>>1; build(u<<1,l,mid);build(u<<1|1,mid+1,r); push_up(u); } void push_down(int u){ if(tr[u].lazy){ tr[u<<1].w=(tr[u<<1].w+tr[u<<1].siz*tr[u].lazy)%MOD; tr[u<<1|1].w=(tr[u<<1|1].w+tr[u<<1|1].siz*tr[u].lazy)%MOD; tr[u<<1].lazy=(tr[u<<1].lazy+tr[u].lazy)%MOD; tr[u<<1|1].lazy=(tr[u<<1|1].lazy+tr[u].lazy)%MOD; tr[u].lazy=0; } } void update(int u,int l,int r,int val){ if(l<=tr[u].l && tr[u].r<=r){ tr[u].w+=tr[u].siz*val; tr[u].lazy+=val; return ; } push_down(u); int mid=(tr[u].l+tr[u].r)>>1; if(l<=mid) update(u<<1,l,r,val); if(r>mid) update(u<<1|1,l,r,val); push_up(u); } void treeadd(int u,int v,int val){ while(top[u]!=top[v]){//將兩個節點跳到同一重鏈上 if(deep[top[u]]<deep[top[v]]) swap(u,v); update(1,dnf[top[u]],dnf[u],val); u=f[top[u]]; } if(deep[u]>deep[v]) swap(u,v);//節點編號的順序依照深度大小,深度越大節點編號越大 update(1,dnf[u],dnf[v],val);//因為遍歷線段樹區間[l,r]必須從小到大 } int query(int u,int l,int r){ int ans=0; if(l<=tr[u].l && tr[u].r<=r) return tr[u].w; push_down(u); int mid=(tr[u].l+tr[u].r)>>1; if(l<=mid) ans=(ans+query(u<<1,l,r))%MOD; if(r>mid) ans=( ans+query(u<<1|1,l,r))%MOD; return ans; } void querysum(int u,int v){//樹剖求區間和 int ans=0; while(top[u]!=top[v]){//不在一條重鏈上時,把深度低的鏈頂到節點連續區間求和 if(deep[top[u]]<deep[top[v]]) swap(u,v); ans=(ans+query(1,dnf[top[u]],dnf[u]))%MOD; u=f[top[u]]; }//跳出循環后,u,v在同一條鏈 if(deep[u]>deep[v]) swap(u,v); ans=(ans+query(1,dnf[u],dnf[v]))%MOD; printf("%d\n",ans); } int main(){ memset(head,-1,sizeof(head)); scanf("%d%d%d%d",&n,&m,&root,&MOD); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n-1;i++){ int x,y; scanf("%d %d",&x,&y); Insert(x,y);Insert(y,x); } dfs1(root,0); dfs2(root,root); build(1,1,n); while(m--){ int op,x,y,z; scanf("%d",&op); if(op==1){ scanf("%d%d%d",&x,&y,&z); z=z%MOD; treeadd(x,y,z); } else if(op==2){ scanf("%d %d",&x,&y); querysum(x,y); } else if(op==3){ scanf("%d %d",&x,&y); update(1,dnf[x],size[x]+dnf[x]-1,y%MOD); } else if(op==4){ scanf("%d",&x); printf("%d\n",query(1,dnf[x],dnf[x]+size[x]-1)); } } }
-