1. 程式人生 > 實用技巧 >樹連剖分

樹連剖分

WPY神仙:什麼時候能行雲流水一次性過樹鏈剖分啊!

轉載於luogu日報

樹鏈剖分就是將樹分割成多條鏈,然後利用資料結構(線段樹、樹狀陣列等)來維護這些鏈。別說你不知道什麼是樹~~ ╮(─▽─)╭

前置知識

  • dfs序
  • LCA 線段樹

先來回顧兩個問題:

1.將樹從\(x\)\(y\)結點最短路徑上所有節點的值都加上z

這也是個模板題了吧

我們很容易想到,樹上差分可以以\(O(n+m)\)的優秀複雜度解決這個問題

2.求樹從\(x\)\(y\)結點最短路徑上所有節點的值之和

lca大水題,我們又很容易地想到,dfs\(O(n)\)預處理每個節點的dis(即到根節點的最短路徑長度)

然後對於每個詢問,求出x,y兩點的lca,利用lca的性質 distance $(x,y)=dis(x)+dis(y)-2*dis(lca) $求出結果

時間複雜度\(O(mlogn+n)\)

現在來思考一個bug:

如果剛才的兩個問題結合起來,成為一道題的兩種操作呢?

剛才的方法顯然就不夠優秀了(每次詢問之前要跑dfs更新dis)

樹剖是通過輕重邊剖分將樹分割成多條鏈,然後利用資料結構來維護這些鏈(本質上是一種優化暴力)

首先明確概念:

  • 重兒子:父親節點的所有兒子中子樹結點數目最多(size最大)的結點;
  • 輕兒子:父親節點中除了重兒子以外的兒子;
  • 重邊:父親結點和重兒子連成的邊;
  • 輕邊:父親節點和輕兒子連成的邊;
  • 重鏈:由多條重邊連線而成的路徑;
  • 輕鏈:由多條輕邊連線而成的路徑;

比如上面這幅圖中,用黑線連線的結點都是重結點,其餘均是輕結點,

2-11就是重鏈,2-5就是輕鏈,用紅點標記的就是該結點所在重鏈的起點,也就是下文提到的top結點,

還有每條邊的值其實是進行dfs時的執行序號。

宣告變數:

int cnt,lnk[maxn],nxt[maxn<<1],to[maxn<<1];//建邊
int a[maxn<<2],lazy[maxn<<2];//線段樹
int son[maxn],w[maxn],id[maxn],tot,deep[maxn],size[maxn],top[maxn],fa[maxn],rk[maxn];//son[maxn]:重兒子,w[maxn]:權值,id[maxn]:樹上節點對應線段樹上的標號,deep[maxn]:節點深度,size[maxn]節點子樹大小,top[maxn]鏈最上端的編號,fa[maxn]父節點,rk[maxn]線段樹上編號對應的樹上節點

樹鏈剖分的實現

1.對於一個點我們首先求出它所在的子樹大小,找到它的重兒子(即處理出size,son陣列)

解釋:比如說點1,它有三個兒子2,3,4

2所在子樹的大小是5

3所在子樹的大小是2

4所在子樹的大小是6

那麼1的重兒子是4

ps:如果一個點的多個兒子所在子樹大小相等且最大,那隨便找一個當做它的重兒子就好了。

葉節點沒有重兒子,非葉節點有且只有一個重兒子

2.在dfs過程中順便記錄其父親以及深度(即處理出f,d陣列),操作1,2可以通過一遍dfs完成

inline void DFS_1(int x,int f,int dep){
	deep[x]=dep;fa[x]=f;size[x]=1;
	int max_son=-1;
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==f)continue;
		DFS_1(to[j],x,dep+1);
		size[x]+=size[to[j]];
		if(size[to[j]]>max_son){max_son=size[to[j]],son[x]=to[j];}
	}
	return ;
}


dfs跑完大概是這樣的,大家可以手動模擬一下

3.第二遍dfs,然後連線重鏈,同時標記每一個節點的dfs序,並且為了用資料結構來維護重鏈,我們在dfs時保證一條重鏈上各個節點dfs序連續(即處理出陣列top,id,rk)

inline void DFS_2(int x,int tp){
	id[x]=++tot;
	rk[tot]=x;
	top[x]=tp;
	if(son[x])DFS_2(son[x],tp);
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==fa[x]||to[j]==son[x])continue;
		DFS_2(to[j],to[j]);
	}
	return ;
}

dfs跑完大概是這樣的,大家可以手動模擬一下

4,兩遍dfs就是樹鏈剖分的主要處理,通過dfs我們已經保證一條重鏈上各個節點dfs序連續,那麼可以想到,我們可以通過資料結構(以線段樹為例)來維護一條重鏈的資訊

回顧上文的那個題目,修改和查詢操作原理是類似的,以查詢操作為例,其實就是個LCA,不過這裡使用了top來進行加速,因為top可以直接跳轉到該重鏈的起始結點,輕鏈沒有起始結點之說,他們的top就是自己。需要注意的是,每次迴圈只能跳一次,並且讓結點深的那個來跳到top的位置,避免兩個一起跳從而插肩而過。

inline void build(int x,int L,int R){
	if(L==R){
		a[x]=w[rk[L]]%TT;
		return ;
	}
	int mid=(R-L>>1)+L;
	build(x<<1,L,mid);
	build(x<<1|1,mid+1,R);
	a[x]=(a[x<<1]+a[x<<1|1])%TT;
	return ;
}

inline void pushdown(int x,int len){
	if(lazy[x]==0)return ;
	lazy[x<<1]=(lazy[x<<1]+lazy[x])%TT;
	lazy[x<<1|1]=(lazy[x<<1|1]+lazy[x])%TT;
	a[x<<1]=(a[x<<1]+lazy[x]*(len-(len>>1)))%TT;
	a[x<<1|1]=(a[x<<1|1]+lazy[x]*(len>>1))%TT;
	lazy[x]=0;
	return ;
}

inline void query(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R){ret=(ret+a[x])%TT;return ;}
	else {
		pushdown(x,r-l+1);int mid=(r-l>>1)+l;
		if(L<=mid)query(x<<1,l,mid,L,R);
		if(R>mid)query(x<<1|1,mid+1,r,L,R);
	}
	return ;
}

inline void update(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R){
		lazy[x]=(lazy[x]+k)%TT;
		a[x]=(a[x]+k*(r-l+1))%TT;
	}
	else{
		pushdown(x,r-l+1);
		int mid=(r-l>>1)+l;
		if(L<=mid)update(x<<1,l,mid,L,R,k);
		if(R>mid)update(x<<1|1,mid+1,r,L,R,k);
		a[x]=(a[x<<1]+a[x<<1|1])%TT;
	}
	return ;
}

inline int qRange(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		ret=0;
		query(1,1,N,id[top[x]],id[x]);
		ans=(ret+ans)%TT;
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	ret=0;query(1,1,N,id[x],id[y]);
	ans=(ans+ret)%TT;
	return ans;
}

inline void upRange(int x,int y,int k){
	k%=TT;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		update(1,1,N,id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	update(1,1,N,id[x],id[y],k);
	return ;
}

inline int qson(int x){
	ret=0;
	query(1,1,N,id[x],id[x]+size[x]-1);
	return ret;
}
inline void upson(int x,int k){
	update(1,1,N,id[x],id[x]+size[x]-1,k);
}

大家如果明白了樹鏈剖分,也應該有舉一反三的能力(反正我沒有),修改和LCA就留給大家自己完成了

5.樹鏈剖分的時間複雜度

樹鏈剖分的兩個性質:

1,如果(u,v)是一條輕邊,那麼\(size(v)<size(u)/2\)

2,從根結點到任意結點的路所經過的輕重鏈的個數必定都小於\(logn\)

可以證明,樹鏈剖分的時間複雜度為\(O(nlogn)\)

例題

1.樹鏈剖分模板

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
typedef long long LL;
int N,M,R,TT;
int  ret;
inline int read(){
	int ret=0,f=1;char ch=getchar();
	while(ch<'0'|ch>'9'){if(ch=='-')f=-f;ch=getchar();}
	while(ch<='9'&&ch>='0')ret=ret*10+ch-'0',ch=getchar();
	return ret*f;
}
int cnt,lnk[maxn],nxt[maxn<<1],to[maxn<<1],w[maxn];
int a[maxn<<2],lazy[maxn<<2];
int son[maxn],id[maxn],tot,deep[maxn],size[maxn],top[maxn],fa[maxn],rk[maxn];

inline void add_e(int x,int y){to[++cnt]=y;nxt[cnt]=lnk[x];lnk[x]=cnt;}

inline void DFS_1(int x,int f,int dep){
	deep[x]=dep;fa[x]=f;size[x]=1;
	int max_son=-1;
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==f)continue;
		DFS_1(to[j],x,dep+1);
		size[x]+=size[to[j]];
		if(size[to[j]]>max_son){max_son=size[to[j]],son[x]=to[j];}
	}
	return ;
}

inline void DFS_2(int x,int tp){
	id[x]=++tot;
	rk[tot]=x;
	top[x]=tp;
	if(son[x])DFS_2(son[x],tp);
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==fa[x]||to[j]==son[x])continue;
		DFS_2(to[j],to[j]);
	}
	return ;
}

inline void build(int x,int L,int R){
	if(L==R){
		a[x]=w[rk[L]]%TT;
		return ;
	}
	int mid=(R-L>>1)+L;
	build(x<<1,L,mid);
	build(x<<1|1,mid+1,R);
	a[x]=(a[x<<1]+a[x<<1|1])%TT;
	return ;
}

inline void pushdown(int x,int len){
	if(lazy[x]==0)return ;
	lazy[x<<1]=(lazy[x<<1]+lazy[x])%TT;
	lazy[x<<1|1]=(lazy[x<<1|1]+lazy[x])%TT;
	a[x<<1]=(a[x<<1]+lazy[x]*(len-(len>>1)))%TT;
	a[x<<1|1]=(a[x<<1|1]+lazy[x]*(len>>1))%TT;
	lazy[x]=0;
	return ;
}

inline void query(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R){ret=(ret+a[x])%TT;return ;}
	else {
		pushdown(x,r-l+1);int mid=(r-l>>1)+l;
		if(L<=mid)query(x<<1,l,mid,L,R);
		if(R>mid)query(x<<1|1,mid+1,r,L,R);
	}
	return ;
}

inline void update(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R){
		lazy[x]=(lazy[x]+k)%TT;
		a[x]=(a[x]+k*(r-l+1))%TT;
	}
	else{
		pushdown(x,r-l+1);
		int mid=(r-l>>1)+l;
		if(L<=mid)update(x<<1,l,mid,L,R,k);
		if(R>mid)update(x<<1|1,mid+1,r,L,R,k);
		a[x]=(a[x<<1]+a[x<<1|1])%TT;
	}
	return ;
}

inline int qRange(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		ret=0;
		query(1,1,N,id[top[x]],id[x]);
		ans=(ret+ans)%TT;
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	ret=0;query(1,1,N,id[x],id[y]);
	ans=(ans+ret)%TT;
	return ans;
}

inline void upRange(int x,int y,int k){
	k%=TT;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		update(1,1,N,id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	update(1,1,N,id[x],id[y],k);
	return ;
}

inline int qson(int x){
	ret=0;
	query(1,1,N,id[x],id[x]+size[x]-1);
	return ret;
}
inline void upson(int x,int k){
	update(1,1,N,id[x],id[x]+size[x]-1,k);
}
int main(){
	N=read();M=read();R=read();TT=read();
	for(int i=1;i<=N;i++)w[i]=read();
	for(int i=1;i<N;i++){
		int x=read(),y=read();
		add_e(x,y);add_e(y,x);
	}
	DFS_1(R,0,1);
	DFS_2(R,R);
	build(1,1,N);
	for(int i=1;i<=M;i++){
		int p,x,y,k;
		p=read();
		if(p==1){x=read(),y=read(),k=read();upRange(x,y,k);}
		if(p==2){x=read();y=read();printf("%d\n",qRange(x,y));}
		if(p==3){x=read();k=read();upson(x,k);}
		if(p==4){x=read();printf("%d\n",qson(x));}
	}
	return 0;
}

WPY神仙:什麼時候能行雲流水一次性過樹鏈剖分啊!

前後呼應