樹連剖分
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神仙:什麼時候能行雲流水一次性過樹鏈剖分啊!
前後呼應