樹鏈剖分演算法詳解
學OI也有一段時間了,感覺該搞點東西了。
於是學習了樹(熟)鏈(練)剖(pou)分(糞)
當然,學習這個演算法是需要先學習線段樹的。不懂的還是再過一段時間吧。
如果碰到一道題,要對一顆樹的兩個點中的最短路徑、以u為根的子樹之類的東西進行修改或者查詢,那麼大概就是樹鏈剖分的題了。
樹鏈剖分就是把一顆樹的節點按照新的順序扔到一顆線段樹裡面,然後保證一條樹鏈上的點線上段樹中儘可能連續。
為什麼是儘可能?因為在一棵樹中,怎麼搞也無法保證對於每一個節點,他的父親編號都是它的-1,所以是儘可能。那麼怎麼儘可能呢?
有很多演算法,今天提到的就是樹鏈剖分。我們把一顆樹上的所有鏈分成輕鏈
而劃分輕鏈和重鏈的依據是:對於每一個節點u,v是它的兒子,v有一個大小,就是size,代表以v為根的子樹的大小。我們選取u最大的兒子為重(zhong)兒子,其餘兒子為輕兒子。以連向重兒子的邊為重邊,剩下的邊為輕邊。
然後所有重邊連成的鏈叫做重鏈,(並不存在輕鏈)比如下圖,紅色的鏈是重鏈(注意,對於一個葉子節點,如果連向它的是一條輕鏈,那麼他自己就是一條重鏈)
這樣,我們把一棵樹劃分成了重鏈和輕鏈,我們能保證所有重鏈都不重不漏的包含了所有的點。
那麼這些重鏈有什麼用?在劃分重鏈的過程中用到的DFS,這個DFS能保證,對於每一條重鏈,他們的DFS序是連續的!
這樣,我們就可以用線段樹(或者其他資料結構)維護了!
現在,我們把熟練剖分化成兩個部分:
1、把樹上的所有點劃分重鏈,然後求出它們的DFS序,以這個順序扔到線段樹裡面。
2、線上段樹上進行維護。
所以,如何實現劃分重鏈?我們需要用兩個DFS,第一個DFS找到所有點的重兒子,第二個DFS將所有重兒子連成重鏈。
第一個DFS:size是以當前點為根的子樹的大小,f是當前點的父親,son是當前點的重兒子。
inline void getson(int u,int fa){//獲取每個節點的重兒子 size[u]=1; for(int e=head[u];e;e=nxt[e])if(to[e]!=fa){ depth[to[e]]=depth[u]+1; f[to[e]]=u; getson(to[e],u); size[u]+=size[to[e]];//記錄以每個節點為根的樹的大小 if(!son[u] || size[son[u]]<size[to[e]]) son[u]=to[e];//判斷後將這個點變為重兒子 } return ; }
第二個DFS:
inline void getdfn(int u,int t){//連成重鏈,其中我們可以保證,對於每一條重鏈,它們的dfn值是連續的。t記錄的是當前鏈的鏈首 top[u]=t;//top記錄當前鏈鏈首 dfn[u]=++cnt;//記錄dfn值,也是線上段樹中的位置 link[cnt]=u;//dfn的逆運算,用於建樹時的初始賦值 if(!son[u]) return ;//如果當前點沒有重兒子,說明是這條重鏈的結束。 getdfn(son[u],t);//繼續走這條重鏈 for(int e=head[u];e;e=nxt[e])//這個相當於走每一條輕鏈 if(to[e]!=son[u] && to[e]!=f[u]) getdfn(to[e],to[e]);//重新開始走每一條重鏈 return ; }
然後,對於線段樹的建樹,是獨立的,我們不用考慮鏈的關係。(input是輸入檔案)
inline void build(int i,int l,int r){//平凡的建樹 tree[i].l=l,tree[i].r=r; if(l==r){ tree[i].sum=input[link[l]]%mod;//link的作用 return ; } int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%mod; return ; }
最後是修改,查詢和修改很像,一起說了。
我們要把u到v路徑上所有的點都+k,那麼我們就把u,v中深的那個,它到它所在重邊的頂端+k。
然後跳過一條輕邊,重複上面的步驟,知道u,v到一條重邊上。
最後把u到v,+k就可以了。
inline void treeadd(int x,int y,int z){//將題中對樹的修改轉化成對線段樹的修改 int tx=top[x],ty=top[y]; while(tx!=ty){//如果兩個點不在一條重鏈上 if(depth[tx]<depth[ty]) swap(x,y),swap(tx,ty);//保證x的重鏈首元素在下方 add(1,dfn[tx],dfn[x],z);//從x一直修改到x所在重鏈的收元素,因為他們在一條重鏈中,所以線上段樹中的位置是連續的。 x=f[tx];//走過一條輕鏈,到上面一個重鏈的末尾 tx=top[x],ty=top[y];//分別更新x、y的重鏈頂端,準備下一次更新 } if(depth[x]<depth[y]) swap(x,y);//現在x、y都到了一條重鏈上了,然後要保證x在下面。 add(1,dfn[y],dfn[x],z);//再只用更新他們所在的鏈就可以了。 return ; } inline int treesum(int x,int y){//將題中對樹查詢得指令改為對線段樹的查詢。 int ans=0; int tx=top[x],ty=top[y]; while(tx!=ty){//這一段和修改幾乎一樣,就是把原本對每一個區間的修改,變為了查詢,其實都一樣。 if(depth[tx]<depth[ty]) swap(tx,ty),swap(x,y); ans=(ans+query(1,dfn[tx],dfn[x]))%mod; x=f[tx]; tx=top[x],ty=top[ty]; } if(depth[x]<depth[y]) swap(x,y); return (ans+query(1,dfn[y],dfn[x]))%mod; }
對於線段樹上的維護,和樸素的線段樹一樣,就不多說了。
如果題目中說要將以i為根的子樹+k,那就直接線上段樹上從dfn[i]到dfn[i]+size[i],+k就可以了。
具體看AC程式碼:(洛谷模板題)
#include <iostream> #include <cstdio> #include <algorithm> #include <cstdlib> #include <cstring> #define in(a) a=read() #define REP(i,k,n) for(int i=k;i<=n;i++) #define MAXN 100010 using namespace std; inline int read(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f; } int n,m,r,mod,input[MAXN]; int total,head[MAXN],to[MAXN<<1],nxt[MAXN<<1]; int size[MAXN],depth[MAXN],f[MAXN],son[MAXN]; int cnt,dfn[MAXN],link[MAXN],top[MAXN]; struct node{ int l,r,sum,lt; }tree[MAXN<<2]; inline void adl(int a,int b){ total++; to[total]=b; nxt[total]=head[a]; head[a]=total; return ; } inline void getson(int u,int fa){//獲取每個節點的重兒子 size[u]=1; for(int e=head[u];e;e=nxt[e]) if(to[e]!=fa){ depth[to[e]]=depth[u]+1; f[to[e]]=u; getson(to[e],u); size[u]+=size[to[e]];//記錄以每個節點為根的樹的大小 if(!son[u] || size[son[u]]<size[to[e]]) son[u]=to[e];//判斷後將這個點變為重兒子 } return ; } inline void getdfn(int u,int t){//連成重鏈,其中我們可以保證,對於每一條重鏈,它們的dfn值是連續的。t記錄的是當前鏈的鏈首 top[u]=t;//top記錄當前鏈鏈首 dfn[u]=++cnt;//記錄dfn值,也是線上段樹中的位置 link[cnt]=u;//dfn的逆運算,用於建樹時的初始賦值 if(!son[u]) return ;//如果當前點沒有重兒子,說明是這條重鏈的結束。 getdfn(son[u],t);//繼續走這條重鏈 for(int e=head[u];e;e=nxt[e])//這個相當於走每一條輕鏈 if(to[e]!=son[u] && to[e]!=f[u]) getdfn(to[e],to[e]);//重新開始走每一條重鏈 return ; } inline void build(int i,int l,int r){//平凡的建樹 tree[i].l=l,tree[i].r=r; if(l==r){ tree[i].sum=input[link[l]]%mod;//link的作用 return ; } int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%mod; return ; } inline void pushdown(int i){//平凡的pushdown if(!tree[i].lt) return ; tree[i<<1].lt+=tree[i].lt; tree[i<<1|1].lt+=tree[i].lt; int mid=(tree[i].l+tree[i].r)>>1; tree[i<<1].sum=(tree[i<<1].sum+(mid-tree[i].l+1)*tree[i].lt)%mod; tree[i<<1|1].sum=(tree[i<<1|1].sum+(tree[i].r-mid)*tree[i].lt)%mod; tree[i].lt=0; return ; } inline void add(int i,int l,int r,int k){//平凡的區間修改 if(tree[i].l>=l && tree[i].r<=r){ tree[i].sum=(tree[i].sum+(tree[i].r-tree[i].l+1)*k)%mod; tree[i].lt+=k; return ; } pushdown(i); if(tree[i<<1].r>=l) add(i<<1,l,r,k); if(tree[i<<1|1].l<=r) add(i<<1|1,l,r,k); tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%mod; return ; } inline int query(int i,int l,int r){//平凡的區間查詢 if(tree[i].l>=l && tree[i].r<=r) return tree[i].sum; int sum=0; pushdown(i); if(tree[i<<1].r>=l) sum=(sum+query(i<<1,l,r))%mod; if(tree[i<<1|1].l<=r) sum=(sum+query(i<<1|1,l,r))%mod; return sum; } inline void treeadd(int x,int y,int z){//將題中對樹的修改轉化成對線段樹的修改 int tx=top[x],ty=top[y]; while(tx!=ty){//如果兩個點不在一條重鏈上 if(depth[tx]<depth[ty]) swap(x,y),swap(tx,ty);//保證x的重鏈首元素在下方 add(1,dfn[tx],dfn[x],z);//從x一直修改到x所在重鏈的收元素,因為他們在一條重鏈中,所以線上段樹中的位置是連續的。 x=f[tx];//走過一條輕鏈,到上面一個重鏈的末尾 tx=top[x],ty=top[y];//分別更新x、y的重鏈頂端,準備下一次更新 } if(depth[x]<depth[y]) swap(x,y);//現在x、y都到了一條重鏈上了,然後要保證x在下面。 add(1,dfn[y],dfn[x],z);//再只用更新他們所在的鏈就可以了。 return ; } inline int treesum(int x,int y){//將題中對樹查詢得指令改為對線段樹的查詢。 int ans=0; int tx=top[x],ty=top[y]; while(tx!=ty){//這一段和修改幾乎一樣,就是把原本對每一個區間的修改,變為了查詢,其實都一樣。 if(depth[tx]<depth[ty]) swap(tx,ty),swap(x,y); ans=(ans+query(1,dfn[tx],dfn[x]))%mod; x=f[tx]; tx=top[x],ty=top[ty]; } if(depth[x]<depth[y]) swap(x,y); return (ans+query(1,dfn[y],dfn[x]))%mod; } int main(){ in(n),in(m),in(r),in(mod); REP(i,1,n) in(input[i]); int a,b; REP(i,1,n-1) in(a),in(b),adl(a,b),adl(b,a); depth[r]; getson(r,0); getdfn(r,r); build(1,1,n); int p,x,y,z; REP(i,1,m){ in(p); if(p==1) in(x),in(y),in(z),treeadd(x,y,z); if(p==2) in(x),in(y),printf("%d\n",treesum(x,y)); if(p==3) in(x),in(z),add(1,dfn[x],dfn[x]+size[x]-1,z);//我們會發現,在樹鏈剖分中,i這顆子樹裡面所有的節點的dfn都是連續的,我們修改u的子樹就是將u到u+size-1修改就可以了。 if(p==4) in(x),printf("%d\n",query(1,dfn[x],dfn[x]+size[x]-1));//查詢同上。 } return 0; }