前段時間學習了下樹鏈剖分,好久沒看了,今天又複習一遍,趕緊寫下來,別又忘了。
我們在資訊學競賽中,有時會碰到這麼一類題型,在一棵樹中,修改兩點之間路徑上的所有邊(或點)上的某個變數(如邊的長度,點的權值等等),然後詢問單個點(或邊)或者兩點之間路徑上的所有點(或邊)的某些性質(如邊權之和,最大邊最小邊等等)。對於這樣的題,往往容易往線段樹上去靠,但是,單單是用線段樹是無法維護每一條鏈的性質的,所以我們需要一種演算法將樹鏈分開來,使得每條鏈可以和線段樹中的一個區間一一對應上。(當然樹鏈剖分遠遠不止這些簡單的應用,也不一定要和線段樹有什麼關係,總之就是將樹鏈剖分開來吧)。
樹鏈剖分有很多種剖分方法,最常用的應該就是輕重邊剖分了吧(在網上大部分介紹的都是這種剖分方法),什麼是輕重邊剖分呢?
我們首先將樹中的邊分為兩部分,輕邊和重邊,記size(U)為以U為根的子樹的節點的個數,令V為U的兒子中size最大的一個(如有多個最大,只取一個),則我們說邊(U,V)為重邊,其餘的邊為輕邊(如所示紅色為重邊,藍色為輕邊)。
我們將一棵樹的所有邊按上述方法分成輕邊和重邊後,我們可以得到以下幾個性質:
1:若(U,V)為輕邊,則size(V)<=size(U)/2。
這是顯然的。
2:從根到某一點的路徑上輕邊的個數不會超過O(logN),(N為節點總數)。
這也是很簡單,因為假設從跟root到v的路徑有k條輕邊,它們是 root->...->v1->...->v2->......->vk->...->v,我們設size(v)=num,顯然num>=1,則由性質1,我們有size(Vk)>=2,size(Vk-1)>=4......size(v1)>=2^k,顯然有2^k<=N,所以k<=log2(N)。
如果我們把一條鏈中的連續重邊連起來,成為重鏈,則一條鏈就變成了輕邊與重鏈交替分段的鏈,且段數是log(N)層級的,則我們可以講重鏈放線上段樹中維護,輕邊可放可不放,為了方便我一般還是放,但是速度就會打一點折扣了。思路就是這麼多,接下來就是具體實現了。
我們需要維護一下值:
siz[v]表示以v為根的子樹的節點總數。
dep[v]表示v的深度。
son[v]表示與v在同一重鏈上的v的兒子節點。
fa[v]表示v的父親節點。
top[v]表示v所在鏈的頂端節點。
w[v]表示節點v線上段樹中的位置。
siz[],son[],fa[],dep[]可以在第一遍dfs中求出來,top[],w[]可在第二遍dfs中求出來。具體過程看代碼吧。
struct edge{ int to; int next;}e[maxn<<1];int box[maxn],cnt,tot;int siz[maxn],top[maxn],son[maxn],dep[maxn],fa[maxn];void init(){ tot=0; son[0]=dep[0]=0; memset(box,-1,sizeof(box)); cnt=0;}void add(int from,int to){ e[cnt].to=to; e[cnt].next=box[from]; box[from]=cnt++;}void dfs(int now,int pre){ siz[now]=1; fa[now]=pre; son[now]=0; dep[now]=dep[pre]+1; int t,v; for(t=box[now];t+1;t=e[t].next) { v=e[t].to; if(v!=pre) { dfs(v,now); siz[now]+=siz[v]; if(siz[son[now]]<siz[v]) { son[now]=v; } } }}void dfs2(int now,int tp){ top[now]=tp; if(son[now]) dfs2(son[now],top[now]); int t,v; for(t=box[now];t+1;t=e[t].next) { v=e[t].to; if(v!=fa[now]&&v!=son[now]) dfs2(v,v); }}
以上是剖分過程,關於如何在樹鏈剖分後維護兩點間路徑的資訊,請看這裡LCA的樹鏈剖分實現
這裡需要注意的是,對於有些題要修改的權值或詢問的權值在點上,有的在邊上,這在剖分時雖然過程沒有變,但在處理的時候是有區別的,具體不同我想在下面兩道題裡體現。
權值在邊上的情況。
http://codeforces.com/problemset/problem/165/D
codeforces 165D Beard Graph
題意:給一棵樹,樹的每條邊有一種顏色,黑色或白色,一開始所有邊均為黑色,有兩個操作:
操作1:將第i條邊變成白色或將第i條邊變成黑色。
操作2 :詢問u,v兩點之間僅經過黑色變的最短距離。
思路:其實這道題可以不用樹鏈剖分,存在更高效的方法,但是一時又想不到更好的例子。
因為是一棵樹,所以兩點之間的路徑是確定的,所以只需要判斷路徑中是否所有的邊均為黑色邊即可,全是黑邊意味著沒有白邊,所以我們可以這麼做,我們將每條邊剖分放入線段樹中後,初始時將所有邊權設為0,對操作1,如果要將一條邊改為黑色,則將線段樹賦值為零,否則分值為1,然後對於操作2,我們只要看兩點間路徑是否權之和為0即可,若為0,返回兩點間距離,否則返回0。
上代碼:
#include <iostream>#include <string.h>#include <stdio.h>#include <algorithm>#define maxn 100010using namespace std;#define mid ((t[p].l+t[p].r)>>1)#define ls (p<<1)#define rs (ls|1)struct tree{ int l,r; int sum;}t[maxn<<2];void pushup(int p){ t[p].sum=t[ls].sum+t[rs].sum;}void build(int p,int l,int r){ t[p].l=l,t[p].r=r,t[p].sum=0; if(l==r) return; build(ls,l,mid); build(rs,mid+1,r);}void add(int p,int x,int val){ if(t[p].l==t[p].r) { t[p].sum+=val; return; } if(x<=mid) add(ls,x,val); else add(rs,x,val); pushup(p);}int query(int p,int l,int r){ if(t[p].l==l&&t[p].r==r) { return t[p].sum; } if(l>mid) return query(rs,l,r); else if(r<=mid) return query(ls,l,r); else return query(ls,l,mid)+query(rs,mid+1,r);}int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn];struct edge{ int to; int next;}e[maxn<<1];int box[maxn],cnt,tot;void init(){ tot=0; son[0]=dep[0]=0; memset(box,-1,sizeof(box)); cnt=0;}void add(int from,int to){ e[cnt].to=to; e[cnt].next=box[from]; box[from]=cnt++;}void dfs(int now,int pre){ siz[now]=1; fa[now]=pre; son[now]=0; dep[now]=dep[pre]+1; int t,v; for(t=box[now];t+1;t=e[t].next) { v=e[t].to; if(v!=pre) { dfs(v,now); siz[now]+=siz[v]; if(siz[son[now]]<siz[v]) { son[now]=v; } } }}void dfs2(int now,int tp){ w[now]=++tot; top[now]=tp; if(son[now]) dfs2(son[now],top[now]); int t,v; for(t=box[now];t+1;t=e[t].next) { v=e[t].to; if(v!=fa[now]&&v!=son[now]) dfs2(v,v); }}int solve(int a,int b){ int f1=top[a],f2=top[b],dist=0; while(f1!=f2) { if(dep[f1]<dep[f2]) { swap(f1,f2); swap(a,b); } dist+=w[a]-w[f1]+1; int tmp=query(1,w[f1],w[a]); if(tmp) return -1; a=fa[f1]; f1=top[a]; } if(a==b) return dist;//注意這裡 else { if(dep[a]>dep[b]) swap(a,b); int tmp=query(1,w[son[a]],w[b]);//注意這裡 if(tmp) return -1; return dist+w[b]-w[a]; }}int Edge[maxn][2];int main(){ int n,q,i,a,b; scanf("%d",&n); init(); for(i=1;i<n;i++) { scanf("%d%d",&Edge[i][0],&Edge[i][1]); add(Edge[i][0],Edge[i][1]); add(Edge[i][1],Edge[i][0]); } build(1,1,n); dfs(1,0); dfs2(1,1); scanf("%d",&q); while(q--) { int k; scanf("%d",&k); if(k==3) { scanf("%d%d",&a,&b); printf("%d\n",solve(a,b)); } else { scanf("%d",&i); int tmp; if(dep[Edge[i][0]]>dep[Edge[i][1]]) tmp=Edge[i][0]; else tmp=Edge[i][1]; if(k==1) add(1,w[tmp],-1); else add(1,w[tmp],1); } } return 0;}
權值在點上的情況:
http://acm.hdu.edu.cn/showproblem.php?pid=3966
HDU:3966 Aragorn's Story
題意:題意很明白,給一棵樹,將兩點之間的路徑中的所有點的權值增加或減少一個數,詢問特定點當前的權值大小。
思路:思路應該很清晰了,將樹剖分後放進線段樹中維護。
代碼如下:
#pragma comment(linker,"/STACK:100000000,100000000")#include <iostream>#include <string.h>#include <stdio.h>#include <algorithm>#define maxn 50010using namespace std;#define mid ((t[p].l+t[p].r)>>1)#define ls (p<<1)#define rs (ls|1)struct tree{ int l,r; int lazy;}t[maxn<<2];int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn],num[maxn],tt[maxn];void pushdown(int p){ if(t[p].lazy) { t[ls].lazy+=t[p].lazy; t[rs].lazy+=t[p].lazy; t[p].lazy=0; }}void build(int p,int l,int r){ t[p].l=l,t[p].r=r,t[p].lazy=0; if(l==r) { t[p].lazy=num[tt[l]]; return; } build(ls,l,mid); build(rs,mid+1,r);}void add(int p,int l,int r,int val){ if(t[p].l==l&&t[p].r==r) { t[p].lazy+=val; return; } pushdown(p); if(r<=mid) add(ls,l,r,val); else if(l>mid) add(rs,l,r,val); else { add(ls,l,mid,val); add(rs,mid+1,r,val); }}int query(int p,int x){ if(t[p].l==t[p].r) { return t[p].lazy; } pushdown(p); if(x>mid) return query(rs,x); else return query(ls,x);}struct edge{ int to; int next;}e[maxn<<1];int box[maxn],cnt,tot;void init(){ tot=0; son[0]=dep[0]=0; memset(box,-1,sizeof(box)); cnt=0;}void add(int from,int to){ e[cnt].to=to; e[cnt].next=box[from]; box[from]=cnt++;}void dfs(int now,int pre){ siz[now]=1; fa[now]=pre; son[now]=0; dep[now]=dep[pre]+1; int t,v; for(t=box[now];t+1;t=e[t].next) { v=e[t].to; if(v!=pre) { dfs(v,now); siz[now]+=siz[v]; if(siz[son[now]]<siz[v]) { son[now]=v; } } }}void dfs2(int now,int tp){ w[now]=++tot; tt[tot]=now; top[now]=tp; if(son[now]) dfs2(son[now],top[now]); int t,v; for(t=box[now];t+1;t=e[t].next) { v=e[t].to; if(v!=fa[now]&&v!=son[now]) dfs2(v,v); }}void solve(int a,int b,int val){ int f1=top[a],f2=top[b]; while(f1!=f2) { if(dep[f1]<dep[f2]) { swap(f1,f2); swap(a,b); } add(1,w[f1],w[a],val); a=fa[f1]; f1=top[a]; } if(a==b) { add(1,w[a],w[a],val);//注意這裡 } else { if(dep[a]>dep[b]) swap(a,b); add(1,w[a],w[b],val);//注意這裡 }}int main(){ freopen("dd.txt","r",stdin); int n,m,q,a,b,c; char str[2]; while(scanf("%d%d%d",&n,&m,&q)!=EOF) { init(); int i; for(i=1;i<=n;i++) { scanf("%d",&num[i]); } for(i=1;i<=m;i++) { scanf("%d%d",&a,&b); add(a,b); add(b,a); } dfs(1,0); dfs2(1,1); build(1,1,n); while(q--) { int node; scanf("%s",str); if(str[0]=='Q') { scanf("%d",&node); printf("%d\n",query(1,w[node])); } else { scanf("%d%d%d", &a,&b,&c); if(str[0]=='I') solve(a,b,c); else solve(a,b,-c); } } } return 0;}
我已將需要注意的地方在代碼中標記下來了,
區別就是在修改最後一條鏈時,也就是a,b在同一條重鏈中時,我們不妨設dep[a]<=dep[b],這時我們知道a是原來我們要求的v,w兩點的LCA。因為我們樹鏈剖分時,將重鏈放入線段樹中時,事實上將點與邊一一對應了,每個點對應於其父節點與其串連的邊,對於根節點,可設定一個虛擬節點,把它看成根節點的父節點。這樣在放入線段樹中的操作就可以不變(其實還是為了實現方便)。如果權值在邊上,那麼我要求v,w兩點間的路徑時,其LCA所對應的邊並不在這條路徑裡,所以我們要少更新一條邊。如果權值在點上,則LCA顯然也在v與w之間的路徑中,則需要更新LCA。這就是兩種題的不同點。
PS:其實樹鏈剖分還有好多應用還有拓展,不過本弱菜還沒有學得到,這裡只是將最基本的應用總結出來,希望各位神牛不要BS。
PS2:DFS寫法容易爆棧,所以還有非遞迴寫法,如BFS寫法和類比棧等等,不過我還沒研究出來。。。