一篇自己都看不懂的樹鏈剖分學習筆記
樹鏈剖分是一個比較好理解的資料結構,碼量不是很大(如果你發現你寫的很多,那麼一定是線段樹的鍋)
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.基礎題
b.較難題
PS:其實樹鏈剖分比較難的還是線上段樹上
HNOI2016 網路(值得注意的是,這道題正解是整體二分,但是三個$log$的樹剖演算法竟然能通過這個題,那就放在這裡算了)
當然,不要忘記了SPOJ的Qtree和Can you answer these queries系列的題目!