1. 程式人生 > >一篇自己都看不懂的樹鏈剖分學習筆記

一篇自己都看不懂的樹鏈剖分學習筆記

樹鏈剖分是一個比較好理解的資料結構,碼量不是很大(如果你發現你寫的很多,那麼一定是線段樹的鍋)

PS:樹鏈剖分預設為重鏈剖分,長鏈剖分和實鏈剖分(LCT)以後有時間再寫

One.樹鏈剖分用來幹啥

樹鏈剖分的基本應用是對一段路徑上或者一棵子樹的點的點權進行修改,並且對路徑或者子樹內的點權進行查詢操作。樹鏈剖分本身的複雜度為$O(nlogn)$,但是因為它經常要與線段樹、樹狀陣列等$log$資料結構共用,所以一般需要用到樹鏈剖分的問題的時間複雜度為$O(nlog^2n)$

Two.前置知識

會dfs序、會線段樹,要求還是很低的

Three.理論 

先約定幾個符號:

$size_i$:以點$i$為根的子樹的大小

$dfn_i$:點$i$在$dfs$時獲得的序號(即點$i$的$dfs$序)

$dep_i$:點$i$的深度

$top_i$:等會兒解釋

資料結構的名字是樹鏈剖分,也就是把一棵樹剖成一條一條的鏈進行維護。其中最講究的還是如何剖鏈。

$For\,example$,對於下面這棵樹,我們對它進行剖分

在樹鏈剖分的模式下,我們標記每一個點與其$size$最大的兒子(稱其為重兒子)的邊(如果沒有兒子就不管),因為這個兒子是最重的,所以稱它為重邊,而其他沒有標記的邊為輕邊,如果有多個$size$同樣最大的兒子,就隨機標記一條。

如果將重邊標紅,那麼上面的那棵樹就會變成下面這樣:

可以發現重邊相連形成了一些重鏈(這棵樹上有$2$條重鏈)

關於輕邊和重鏈有一個很重要的性質:

每一個點到根所經過的輕邊和重鏈的數量不超過$logn$條(其中$n$為點數)

證明:對於輕邊,考慮子樹大小。假如從$i$號點通過一條輕邊跳到了其父親$j$號點,那麼意味著至少存在一個不為$i$的$j$的兒子$k$滿足$size_k \geq size_i$,那麼$size_j \geq size_i \times 2$,也就是說從$i$走到$j$,子樹大小至少翻倍,那麼經過$x$條輕邊到達的點的子樹大小至少為$2^x$,所以$2^x \leq n$,即$x \leq logn$。因為重鏈是被輕邊分開的,所以重鏈的數量也不會超過$logn$條。

這一個結論意味著如果我們可以將重鏈上的操作變為$O(1)$或者$O(logn)$的話,時間複雜度就會十分優秀。

在區間修改之前,考慮一個問題:如何在樹鏈剖分的模式下求$LCA$

考慮將每一個點所在的重鏈中深度最淺的點,也就是鏈頂$top$記下來,就像下面這樣

(這裡手滑了,$top_8$應該等於$8$)

以發現:我們可以通過$top\,O(1)$地跳一條重鏈了!那麼根據上面的分析,我們求$LCA$的複雜度也就是$O(logn)$了,複雜度與倍增一致,但是樹鏈剖分常數更小(因為實際上很難跳滿$logn$條鏈)

總結一下求$x,y$兩點$LCA$的操作步驟:

1、獲得$top_x$和$top_y$

2、如果$top_x==top_y$轉步驟5,否則轉步驟3

3、如果$dep_{top_x}<dep_{top_y}$,交換$x$和$y$

4、令$x=fa_{top_x}$,跳過$x$所在的重鏈和重鏈頂的輕邊,轉步驟1

5、如果$dep_x<dep_y$,則$LCA(x,y)=x$,否則為$y$

不是很好理解的是步驟3,為什麼要比較$top$的$dep$的大小而不是本身的$dep$的大小?留給讀者自證

會跳LCA了,接下來考慮路徑和子樹的修改。

對於樹上的區間修改我們常用$DFS$序+區間修改資料結構進行配合,在樹鏈剖分中也是一樣的,但是因為我們需要優化重鏈上的修改,所以$DFS$的順序十分重要。

我們每一次$DFS$到一個點,優先向其重兒子$DFS$,這樣可以得到一個特別的$dfn$序:

可以發現一條重鏈上的$dfn$是連續的,這意味著我們在對一條路徑做修改的時候,在上面求兩點$LCA$的過程中對跳過的重鏈的部分進行區間修改就可以了,查詢同理。使用線段樹、樹狀陣列等資料結構可以對這一些修改和查詢進行維護。

而子樹的$dfn$序顯然也是連續的。

那麼我們最重要的修改和查詢問題就解決了o(* ̄▽ ̄*)ブ

Four.實現的一些細節

一般樹鏈剖分的實現是兩個$dfs$:

第一個$dfs$處理$dep$、$fa$、$size$和重兒子

第二個$dfs$處理$top$和$dfn$

Five.一個例題

例題當然要是Luogu的樹鏈剖分模板題

其實操作就是上面的操作qwq

  1 #include<bits/stdc++.h>
  2 #define MAXN 100001 
  3 using namespace std;
  4 inline int read(){
  5     int a = 0;
  6     char c = getchar();
  7     while(!isdigit(c))
  8         c = getchar();
  9     while(isdigit(c)){
 10         a = (a << 3) + (a << 1) + (c ^ '0');
 11         c = getchar();
 12     }
 13     return a;
 14 }
 15 
 16 int forOutput[12];
 17 inline void print(int a){
 18     int dirN = 0;
 19     while(a){
 20         forOutput[dirN++] = a % 10;
 21         a /= 10;
 22     }
 23     if(dirN == 0)
 24         putchar('0');
 25     while(dirN)
 26         putchar('0' + forOutput[--dirN]);
 27     putchar('\n');
 28 }
 29 
 30 struct node{
 31     int l , r , sum , mark;
 32 }SegTree[MAXN << 2];
 33 struct Edge{
 34     int end , upEd;
 35 }Ed[MAXN << 1];
 36 int N , M , R , P , ts , cntEd;
 37 int fa[MAXN] , size[MAXN] , son[MAXN] , ind[MAXN] , rk[MAXN] , depth[MAXN] , top[MAXN] , val[MAXN] , head[MAXN] , maxInd[MAXN];
 38 
 39 inline int max(int a , int b){
 40     return a > b ? a : b;
 41 }
 42 
 43 //加邊
 44 inline void addEd(int a , int b){
 45     Ed[++cntEd].end = b;
 46     Ed[cntEd].upEd = head[a];
 47     head[a] = cntEd;
 48 }
 49 
 50 //第一個dfs,處理size、son、fa和dep
 51 void dfs1(int dir , int dep , int father){
 52     depth[dir] = dep;
 53     fa[dir] = father;
 54     size[dir] = 1;
 55     for(int i = head[dir] ; i ; i = Ed[i].upEd)
 56         if(Ed[i].end != father){
 57             dfs1(Ed[i].end , dep + 1 , dir);
 58             size[dir] += size[Ed[i].end];
 59             if(size[son[dir]] < size[Ed[i].end])
 60                 son[dir] = Ed[i].end;
 61         }
 62 }
 63 
 64 //第二個dfs,處理top和ind
 65 //注意到那個rk了嗎?rk[i]表示的是dfs序為i的點在原樹中的編號,這樣線上段樹的初始化部分就可以直接帶入點權了。
 66 void dfs2(int dir , int t){
 67     top[dir] = t;
 68     maxInd[dir] = ind[dir] = ++ts;
 69     rk[ts] = dir;
 70     if(!son[dir])
 71         return;
 72     dfs2(son[dir] , t);
 73     maxInd[dir] = max(maxInd[dir] , maxInd[son[dir]]);
 74     for(int i = head[dir] ; i ; i = Ed[i].upEd)
 75         if(Ed[i].end != son[dir] && Ed[i].end != fa[dir]){
 76             dfs2(Ed[i].end , Ed[i].end);
 77             maxInd[dir] = max(maxInd[dir] , maxInd[Ed[i].end]);
 78         }
 79 }
 80 
 81 //線段樹更新資訊
 82 inline void pushup(int dir){
 83     SegTree[dir].sum = (SegTree[dir << 1].sum + SegTree[dir << 1 | 1].sum) % P;
 84 }
 85 
 86 //線段樹下放標記
 87 inline void pushdown(int dir){
 88     if(SegTree[dir].mark){
 89         SegTree[dir << 1].sum = (SegTree[dir << 1].sum + (SegTree[dir << 1].r - SegTree[dir << 1].l + 1) * SegTree[dir].mark) % P;
 90         SegTree[dir << 1 | 1].sum = (SegTree[dir << 1 | 1].sum + (SegTree[dir << 1 | 1].r - SegTree[dir << 1 | 1].l + 1) * SegTree[dir].mark) % P;
 91         SegTree[dir << 1].mark = (SegTree[dir << 1].mark + SegTree[dir].mark) % P;
 92         SegTree[dir << 1 | 1].mark = (SegTree[dir << 1 | 1].mark + SegTree[dir].mark) % P;
 93         SegTree[dir].mark = 0;
 94     }
 95 }
 96 
 97 //線段樹初始化
 98 void init(int dir , int l , int r){
 99     SegTree[dir].l = l;
100     SegTree[dir].r = r;
101     if(l == r)
102     //rk在這裡起作用!
103         SegTree[dir].sum = val[rk[l]] % P;
104     else{
105         init(dir << 1 , l , l + r >> 1);
106         init(dir << 1 | 1 , (l + r >> 1) + 1 , r);
107         pushup(dir);
108     }
109 }
110 
111 //線段樹修改
112 void change(int dir , int l , int r , int mark){
113     if(SegTree[dir].l >= l && SegTree[dir].r <= r){
114         SegTree[dir].mark = (SegTree[dir].mark + mark) % P;
115         SegTree[dir].sum = (SegTree[dir].sum + (SegTree[dir].r - SegTree[dir].l + 1) * mark) % P;
116         return;
117     }
118     pushdown(dir);
119     if(l <= SegTree[dir].l + SegTree[dir].r >> 1)
120         change(dir << 1 , l , r , mark);
121     if(r > SegTree[dir].l + SegTree[dir].r >> 1)
122         change(dir << 1 | 1 , l , r , mark);
123     pushup(dir);
124 }
125 
126 //線段樹查詢和
127 int getSum(int dir , int l , int r){
128     if(SegTree[dir].l >= l && SegTree[dir].r <= r)
129         return SegTree[dir].sum;
130     pushdown(dir);
131     int sum = 0;
132     if(l <= SegTree[dir].l + SegTree[dir].r >> 1)
133         sum = (sum + getSum(dir << 1 , l , r)) % P;
134     if(r > SegTree[dir].l + SegTree[dir].r >> 1)
135         sum = (sum + getSum(dir << 1 | 1 , l , r)) % P;
136     return sum;
137 }
138 
139 //邊跳邊修改的路徑操作
140 inline void work1(int x , int y , int z){
141     int fx = top[x] , fy = top[y];
142     while(fx != fy){
143         if(depth[fx] >= depth[fy]){
144             //將跳過的重鏈進行修改,下同
145             change(1 , ind[fx] , ind[x] , z);
146             x = fa[fx];
147             fx = top[x];
148         }
149         else{
150             change(1 , ind[fy] , ind[y] , z);
151             y = fa[fy];
152             fy = top[y];
153         }
154     }
155     //將最後一段修改
156     if(ind[x] <= ind[y])
157         change(1 , ind[x] , ind[y] , z);
158     else
159         change(1 , ind[y] , ind[x] , z);
160 }
161 
162 //邊跳邊算答案
163 inline int work2(int x , int y){
164     int fx = top[x] , fy = top[y] , sum = 0;
165     while(fx != fy){
166         if(depth[fx] >= depth[fy]){
167             //邊跳邊加入答案
168             sum = (sum + getSum(1 , ind[fx] , ind[x])) % P;
169             x = fa[fx];
170             fx = top[x];
171         }
172         else{
173             sum = (sum + getSum(1 , ind[fy] , ind[y])) % P;
174             y = fa[fy];
175             fy = top[y];
176         }
177     }
178     //將最後一段算入答案
179     if(ind[x] <= ind[y])
180         sum = (sum + getSum(1 , ind[x] , ind[y])) % P;
181     else
182         sum = (sum + getSum(1 , ind[y] , ind[x])) % P;
183     return sum;
184 }
185 
186 //子樹修改與查詢
187 void work3(int x , int z){
188     change(1 , ind[x] , maxInd[x] , z);
189 }
190 
191 int work4(int x){
192     return getSum(1 , ind[x] , maxInd[x]);
193 }
194 
195 int main(){
196     N = read();
197     M = read();
198     R = read();
199     P = read();
200     for(int i = 1 ; i <= N ; i++)
201         val[i] = read();
202     for(int i = 1 ; i < N ; i++){
203         int a = read() , b = read();
204         addEd(a , b);
205         addEd(b , a);
206     }
207     dfs1(R , 1 , 0);
208     dfs2(R , R);
209     init(1 , 1 , N);
210     while(M--){
211         int a = read() , b = read() , c , d;
212         switch(a){
213             case 1:
214                 c = read();
215                 d = read();
216                 work1(b , c , d);
217                 break;
218             case 2:
219                 c = read();
220                 print(work2(b , c));
221                 break;
222             case 3:
223                 c = read();
224                 work3(b , c);
225                 break;
226             case 4:
227                 print(work4(b));
228         }
229     }
230     return 0;
231 }            
樹剖模板

Six.一些練習題

a.基礎題

HEOI/TJOI2016 樹

HAOI2015 樹上操作

SHOI2012 魔法樹

SDOI2011 染色

NOI2015 軟體包管理器

月下“毛景樹”

b.較難題

PS:其實樹鏈剖分比較難的還是線上段樹上

NOIP2018D2T3的動態DP模板

HNOI2016 網路(值得注意的是,這道題正解是整體二分,但是三個$log$的樹剖演算法竟然能通過這個題,那就放在這裡算了)

睡覺綜合困難徵

當然,不要忘記了SPOJ的Qtree和Can you answer these queries系列的題目!