<style> .reveal .slides { text-align: left; font-size:29px; } </style> # Tree Algorithm Introduction to Competitive Programming 2022/04/20 ---- * 樹DP (DP on Tree) * 啟發式合併 (DSU on Tree) * 樹鏈剖分 (Heavy-Light Decomposition) --- ## 樹DP ### (DP on tree) 為了保持DP順序,樹DP通常以**遞迴求解** $DP[i]$ 通常代表以第 $i$ 個點為根的子樹最佳答案 * 一般樹DP * 換根DP ---- ## 例題 ### 樹上最大獨立集 給你一顆 $N (1 \le N \le 10^5)$ 個點的樹, 求最多能同時選多少點,使得選的點彼此都沒有連邊 ![](https://i.imgur.com/1sxHZfJ.png =300x) ---- ### DP 狀態 DP[$x$][0/1] 以節點 $x$ 為根的子樹的最佳解, 第二個維度為要不要選第 $x$ 個節點(0則不選、1代表要選) 對於節點 $x$ 的子樹,分別推出以下兩種狀態 ($i$ 為節點 $x$ 的兒子) DP[$x$][0] = $\sum$ max(DP[i][0],DP[i][1]) DP[$x$][1] = $1+\sum$ DP[i][0] ---- ### DP[x][0] 狀態為 0 代表,不選節點 $x$ 則子節點可選可不選 DP[x][0] = $\sum$ max(DP[i][0], DP[i][1]) ### DP[x][1] 狀態為 1 代表,選節點 $x$ ,則子節點不能選(子節點狀態為 0) DP[x][1] = $1+\sum$ DP[$i$][0] ---- ## DP 計算順序 DP[x][0] = $\sum$ max(DP[i][0],DP[i][1]) DP[x][1] = $1+\sum$ DP[i][0] 在求 DP[x][0/1] 時,會需要用到子節點的 DP 值 因此會先遞迴到最深計算從葉節點開始算 節點 $i$ 的子節點都計算完 DP 值時,即可計算自己的 DP 值 ---- 實作時邊遞迴邊計算 DP 值 ```cpp= int dp[MXN][2]; void dfs(int x,int f){ dp[x][1] = 1; // 狀態[1] 計算自己數量 +1 for(int i:edge[x]){ if(i == f) continue; dfs(i, x); // 先計算完子節點的答案再算自己的 dp[x][0] += max(dp[i][0], dp[i][1]); dp[x][1] += dp[i][0]; } } ``` ---- ## 複雜度 O(N) N個節點,轉移次數為邊的數量(N-1) ---- ### 換根 DP 通常會有兩次 DFS 第一次 DFS 預處理以每個節點 i 為根的子樹的 DP 值 第二次 DFS 使用計算好的 DP 值開始換根,計算分別每個節點為根的 DP 值 ---- ## 例題 給你一棵有 $N$ 個點的樹,求樹上全點對距離總和 即求 $\sum\limits_{i=1}^{n} \sum\limits_{j=i+1}^{n}$ distance$(i,j)$ ![](https://i.imgur.com/leUyDTO.png) 以上圖為例總合為 (1+1+1+2)+(2+2+3)+(2+1)+(3) = 18 ---- ### 暴力作法 * $O(N^3)$ 窮舉每個點對,DFS O(N)暴力計算兩點的距離 * $O(N^2lgN)$ 窮舉每個點對,使用 LCA O(lgN)計算兩點距離 * $O(N^2)$ 每個點當起點DFS,走到每個點把總和記下來 ---- ### $O(N)$ 樹DP DFS 兩次 第一次 DFS 選一個點當根, 設 dp[i] 為以第 i 個節點為根的子樹 所有節點到自己的距離總和 第二次 DFS 遞迴到每個點,用剛剛算好的 DP 值 計算以每個點為整顆樹的根節點的答案 ---- ### 第一次 DFS 算每個節點的子樹到自己的距離總和,以及子樹大小 dp[x] 為以節點 x 為根的子樹到節點i的距離總和 sz[x] 為以節點 x 為根的子樹節點數量 ![](https://i.imgur.com/979PBhv.png =300x) <div style="font-size: 23px"> dp[x] = $\sum$ (dp[i]+sz[i]) 也就是 x 的所有子節點i的子樹距離總和 + 子樹的每個節點再多走一步 dp[1] = (dp[2]+sz[2]) + (dp[3]+sz[3]) + (dp[4]+sz[4]) = 5 dp[2] = 0 dp[3] = (dp[5]+sz[5]) = 1 dp[4] = 0 dp[5] = 0 </div> ---- ### 第二次DFS 從根節點開始計算以每個節點為根的距離總和 根節點1到每個節點的距離總和就是 dp[1] ![](https://i.imgur.com/Q711BkC.png) ---- ### 換根 <div style="font-size: 23px"> 從根節點遞迴下去到節點3當根節點 要計算以節點 3 為根的距離為 從根節點方向的距離總和+本身子樹到自己距離總和 </div> ![](https://i.imgur.com/1aB5xG6.png) ---- #### 從根節點方向的距離總和 在從根節點遞迴時傳下來 (節點 1 的 dp 值) + (節點 3 的貢獻) - (從根節點方向的節點從節點 1 多走到節點 3 的距離) dp[1] - (dp[3]+sz[3]) + (n-sz[3]) #### 本身子樹到自己距離總和 即為 dp[3] ![](https://i.imgur.com/1aB5xG6.png) ---- ### sample code ```cpp= void dfs2(int x,int f,ll sum){ ans += sum + dp[x]; //所有點到結點x距離總和為父節點方向距離總和 + 子樹到自己距離總和 for(int i:edge[x]){ if(i == f) continue; //tmp 為從父節點x到子節點i的距離總合為 ll tmp = sum //x的父節點總和 sum 到結點x的距離 + dp[x] - (dp[i]+sz[i]) //加上x的子樹(除了i方向)到x的距離總和 + (n - sz[i]); //加上從節點x到節點i的距離 dfs2(i, x, tmp); } } ``` ---- ### main function 總複雜度 $O(N)$ ```cpp= int main(){ build_tree(); // O(N) dfs1(1,1); // O(N) dfs2(1,1,0); // O(N) cout<<tot<<endl; } ``` --- ## 啟發式合併 ### (Disjoint Set Union-find on tree) 並查集一般寫法下 單筆操作複雜度為 $O(alpha(n))$ 趨近於常數 而持久化並查集不能路徑壓縮,使得單筆操作複雜度提升到 $O(N)$ 因此要使用啟發式合併來降低複雜度,單筆操作複雜度為 $O(lgN)$ ---- ### find 函式 路徑壓縮 ```cpp= int find(int x){ if(f[x] == x) return x; return f[x] = find(f[x]); } ``` 少了路徑壓縮 最差複雜度為 $O(N)$ ```cpp= int find(int x){ if(f[x] == x) return x; return find(f[x]); } ``` ---- ### 啟發式合併 在直覺上 每次合併兩個集合時, 把小的集合往大的集合合併 ---- 以節點 1 為根的集合 與 節點 5 為根的集合合併 ![](https://i.imgur.com/rHwH1O5.png) ---- 把高度較小的合併到高度較大的 ![](https://i.imgur.com/7Uvsr6v.png) 合併後高度為 2 ---- 把高度較大的合併到高度較小的 ![](https://i.imgur.com/OqBARf1.png) 合併後高度為 3 ---- 因此合併時用高度判斷可以降低 find() 函數找根節點的時間 但降低多少? 來看看兩種極端的 case ---- ### (1) 當兩邊樹高不同時,把高度小的合併到高度大的不會增加高度 <div style="position: absolute; right: 70%; top:100%;"> ![](https://i.imgur.com/7IbNCY8.png =300x) </div> <div style="position: absolute; right: 10%; top:100%;"> ![](https://i.imgur.com/JLbp4hb.png =300x) </div> ---- ### (2) 當兩邊樹高相同時,高度會增加 <div style="position: absolute; right: 70%; top:100%;"> ![](https://i.imgur.com/dD394DM.png =300x) </div> <div style="position: absolute; right: 10%; top:100%;"> ![](https://i.imgur.com/3lbYNSo.png =300x) </div> ---- 因此可以發現兩種情況中,如果每次合併都是第一種 則複雜度不會改變 把小的合併到大的,那[均攤複雜度](https://zh.wikipedia.org/wiki/%E5%B9%B3%E6%91%8A%E5%88%86%E6%9E%90)為 $O(N)$ 但如果每次都是第二種,複雜度會變多少? ---- 答案是 $O(NlgN)$ 合併順序:黑色邊 $\to$ 紅色邊 $\to$ 綠色邊 每次合併都是相同高度的樹,高度都會+1 而這種情況高度每+1,節點數量都會變兩倍 因此高度為 h 的樹,會有 $2^h$ 個節點 ![](https://i.imgur.com/tperu4M.png) 反推回來,如果有 $N$ 個節點,最高高度為 $lgN$ 而會發現其實最差情況用高度和節點數量去判斷會等價 因此我們通常使用子樹大小去判斷 ---- 因此透過每次小的集合合併到大的集合 最差複雜度為 $O(NlgN)$ ```cpp= int f[MXN], sz[MXN]; // 多紀錄子樹大小 void merge(int x,int y){ x = find(x), y =find(y); if(sz[x] < sz[y]) swap(x,y); sz[x] += sz[y]; f[y] = x; } void init(){ //初始化一開始大小都為1 (每群一開始都是獨立一個) for(int i=1;i<=n;i++) sz[i] = 1; for(int i=1;i<=n;i++) f[i] = i; } ``` ---- ## 樹上啟發式合併 #### 例題 - Lomsat gelral 給一棵大小為n(n<=1e5) 的有根樹 每個節點上都有塗上一種顏色 問以 1~n 為根的子樹中,出現最多次的顏色數量為何? #### 作法 當需要用到子樹的資訊,每次去合併時 如果每次都把小的合併到大的,最差複雜度 $O(NlgN)$ ---- 用一個map紀錄不同顏色的數量 最差情況:每個節點顏色都不同 啟發式合併 $O(NlgN)$ * map複雜度 $O(lgN)$ 複雜度為 $O(NlgNlgN)$ 各種實作方法:https://codeforces.com/blog/entry/44351 --- ### 樹上路徑詢問、修改 Q 筆操作 - 詢問點 $u$ 到點 $v$ 路徑上的權重總和 - 更新點 $u$ 到點 $v$ 路徑上的權重加值 如果只有第一種操作可以用 LCA 做 如果只有第二種操作,最後詢問每個節點的權重總和? ---- ### 樹上差分 給你一棵樹,樹上每個節點一開始權重為 0 Q筆操作,每次操作給你一條節點 $x$ 到節點 $y$ 的路徑, 在這條路徑上的每個節點加值 $v$ 最後問你每個節點的值為多少? ---- ### 樹上差分 ![](https://i.imgur.com/ZEhIOQS.png =280x) 假設是節點9到節點5的路徑 可以用一個陣列紀錄加值 cnt[9] += v cnt[5] += v 以及在 lca 紀錄減值 cnt[lca(5,9)] -= v cnt[fa[lca(5,9)]] -= v ---- ![](https://i.imgur.com/ZEhIOQS.png =300x) 代表 從 9 往上的路徑每個點都要加 v,從 5 往上的路徑每個都要加 v 一直加到 lca 為止,而到 lca 會加兩次(從5、9),因此要扣掉一次 接著在把 lca 的父節點把剩下的一次再扣掉以免加到沒走到的路徑 ---- ![](https://i.imgur.com/ZEhIOQS.png =300x) 加值後從子樹遞迴回來,從節點 9 開始紀錄加的值並往上回傳 ```cpp= int dfs(int x,int f){ int ret = cnt[x]; for(int i:edge[x]){ if(i == f) continue; ret += dfs(i, x); } ans[x] = ret; return ret; } ``` ---- Q 筆操作,每次為其中一種操作 - 詢問點 $u$ 到點 $v$ 路徑上的權重總和 - 更新點 $u$ 到點 $v$ 路徑上的權重加值 ---- ## 樹鏈剖分 ### (Heavy Light Decomposition) <div style="font-size:25px"> 樹鏈剖分,將樹分成很多條鏈, 對每一條鏈用資料結構去維護(如線段樹、樹狀數組之類) 有兩種剖分的方法:長鏈剖分、輕重鏈剖分 由於長鏈剖分不太會出現,且複雜度較差(每次經過最多 $\sqrt{N}$ 條鏈) 所以這邊只介紹輕重鏈剖分(每次經過最多 $lgN$ 條鏈) </div> ---- ### 名詞介紹 <div style="font-size:25px;"> **重兒子**:每個點的子樹中,子樹大小(即節點數)最大的子節點 **輕兒子**:除重兒子外的其他子節點 **重邊**:每個節點與其重兒子間的邊 **輕邊**:每個節點與其輕兒子間的邊 **重鏈**:重邊連成的鏈 **輕鏈**:輕邊連成的鏈 </div> <div style=" position: absolute; top: 150px; right: -50px; width: 600px; height: 300px;"> ![](https://i.imgur.com/Tn7Vduf.png =400x) </div> ---- <div style="background-color:white"> ![image alt](https://web.ntnu.edu.tw/~algo/HeavyPathDecomposition1.png =1000x) </div> ---- ![](https://i.imgur.com/SObRPLi.png =400x) 此棵樹會分成 5 條鏈 [1, 2, 5, 6, 11], [3, 9 ,10], [4, 12], [7], [8] 再把每條鏈分別當成一個序列,對序列建成線段樹、BIT等資料結構 ---- ![](https://i.imgur.com/pUlEbxu.png =300x) 當要路徑更新或路徑加值時,以節點8-節點12的路徑為例 會更新到 [8,3,1,4,12] 5個節點 也就等於更新到四段區間 [8-8] [3-3] [1-1] [4-12] 從詢問的節點所屬的鏈開始更新,接著往上一條鏈更新 直到更新到高度最低的節點為止 ---- ![](https://i.imgur.com/ZLyxrJf.png =400x) 另一個例子,節點9-節點6的路徑 更新到 [9,3,1,2,5,6] 6個節點 等於更新到兩個區間 [9-3] [1,6] 兩段 ---- ### 最多會經過幾條鏈? ![](https://i.imgur.com/SObRPLi.png =400x) 假設當前鏈的節點數量為 $k$, 更新或詢問時要移動到上一條鏈,代表父節點的子節點中 必定有另一個子節點其子樹 $\ge k$,因此每次往上的子樹大小必定 $\ge 2k+1$ 每次都至少變2倍,到根節點最多不會超過 $lgN$ 條 ---- ### 複雜度 #### 預處理 $O(N)$ 樹鍊剖分 + $lgN$ 條鏈每條花 $O(N)$ 建線段樹 #### 操作 Q筆操作,每次更新/詢問最多經過 $lgN$ 條鏈 而每次資料結構更新/詢問複雜度 $lgN$ #### 總複雜度 $O(NlgN + QlgNlgN)$ ---- 作法 - 兩次DFS: 1. 找重兒子 2. 樹鏈剖分 3. 對每條鏈建資料結構 ---- ### 第一次 dfs 記錄子樹大小、深度、父節點、重兒子等資訊 子樹大小 找重兒子時候用 深度 建資料結構時,建立 index 父節點 在遍歷鏈的時候會用到 重兒子 用於樹鏈剖分 ```cpp= int sz[MXN],dep[MXN],fa[MXN],heavy[MXN]; ``` ---- ### 找重兒子 <div style="margin-left:-180px"> ```cpp= //假設1-base,並且0為指向空節點,sz[0]=0 void dfs1(int x,int f,int d){ dep[x]=d; //紀錄深度 fa[x]=f; //設父節點 sz[x]=1; //將自己本身加進子樹大小 heavy[x]=0; //一開始先指向空 for(int i=0;i<edge[x].size();i++){ if(edge[x][i]==f) continue; dfs1(edge[x][i],x,d+1); //dfs下去紀錄每個子樹的大小 sz[x]+=sz[edge[x][i]]; //將子樹大小加至自己 if(sz[edge[x][i]]>sz[heavy[x]]) heavy[x]=edge[x][i]; //找重兒子 } } ``` </div> ---- ### 記錄每條鏈的資訊 ![](https://i.imgur.com/teFEMxF.png =300x) 在第二次DFS時計算要建立多少條鏈,以及每條鏈多長 可以知道每條鏈的第一個節點是根節點或者所有的輕兒子 而我們可以用 root 陣列紀錄每個節點所屬的鏈第一個元素是誰 把這條鏈的資訊(長度)都儲存在這條鏈的第一個節點上 ---- ### 樹鏈剖分 <div style="margin-left:-180px"> ```cpp= int treeSz=1; int root[MXN], len[MXN]; void dfs2(int x,int f,bool isLight){ for(int i=0;i<edge[x].size();i++){ if(edge[x][i]==f) continue; if(edge[x][i]==heavy[x]) //判斷是否為重兒子 root[edge[x][i]]=root[x]; //若為重兒子則跟自己同一條鏈 else root[edge[x][i]]=edge[x][i],treeSz++; //否則剖分成新鏈 dfs2(edge[x][i],x); } len[root[x]]++; //紀錄每條鏈的長度儲存在第一節點的位置 } ``` </div> ---- ### 對每條鏈建資料結構 在第二次 DFS 時,有記錄有多少條鏈以及每條鏈的長度 <div style="margin-left:-180px"> ```cpp= vector<int> treeID; vector<vector<int>> tree; void buildTree(){ tree.resize(treeSz); for(int i=1,j=0;i<=n;i++){ if(root[i]==i){ //判斷鏈的開頭 treeID[i]=j++; //設為第j條鏈 tree[treeID[i]].resize(len[i]*4,0); //以線段樹為例 } //設第j條鏈的長度4倍大小 } } ``` </div> ---- ### 路經修改、詢問 <div style="font-size:24px"> 可以從詢問的兩個節點開始更新區間,直到兩個節點都屬於同一條鏈為止 ![](https://i.imgur.com/SrztjRG.png =300x) 以節點9到節點7的路徑為例,分別更新各自的鏈,直到兩個節點最後連到同一條鏈為止 每次判斷兩個當前節點的第一節點深度,優先更新較深節點 x = 9, y = 7 y 所屬的鏈第一個節點深度較深dep[3]<dep[7],更新區間 [7,7],跳到節點 5 x = 9, y = 5 x 所屬的鏈第一個節點深度較深dep[3]>dep[1],更新區間 [9,3],跳到節點 1 接著x, y屬於同一條鏈,更新兩個節點的路徑區間 </div> ---- <div style="margin-left:-180px"> ```cpp= void updatePath(int x,int y,int v){ while(root[x]!=root[y]){ //若不在同一條鏈上 if(dep[root[x]]>dep[root[y]]){ //優先更新深度深的鏈 update(treeID[root[x]],0,dep[x]-dep[root[x]],v); x=fa[root[x]]; } else{ update(treeID[root[y]],0,dep[y]-dep[root[y]],v); y=fa[root[y]]; } } int mn=min(dep[x],dep[y]),mx=max(dep[x],dep[y]); //更新第treeID[i]樹從mn~mx的範圍加值v update(treeID[root[x]],mn,mx,v); //最後會連至同一條鏈上,更新範圍 } ``` </div> ---- ### 複雜度分析 由於剖分的性質,會使任一個點至根結點最多經過$lgN$條鏈, 每條鏈上的詢問、修改為$O(lgN)$ 因此每筆詢問、修改為$O(lgNlgN)$ Q筆詢問則複雜度為$O(QlgNlgN)$ ---- 用樹鏈剖分找最近共同祖先 ```cpp= void getLca(int x,int y){ while(root[x]!=root[y]){ if(dep[root[x]]>dep[root[y]]) x=fa[root[x]]; else y=fa[root[y]]; } return dep[x]>dep[y]?y:x; } ``` <div style="font-size:25px"> 最多經過 $lgN$ 條鏈,因此複雜度為 $O(lgN)$ </div> --- ### Question Time and Practice [Homework Link](https://vjudge.net/contest/489957) 提交期限 5/4,下下星期三 23:59 截止
{"metaMigratedAt":"2023-06-16T23:24:48.623Z","metaMigratedFrom":"YAML","title":"Tree Algorithm","breaks":true,"contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":13474,\"del\":2156}]"}
    1218 views