樹分治
點分治
點分治適合處理大規模的樹上路徑信息問題。
例題 1 Luogu P3806【模板】點分治 1
給定一棵有 \(n\) 個點的帶邊權樹,\(m\) 次詢問,每次詢問給出 \(k\),詢問樹上距離為 \(k\) 的點對是否存在。
\(n\le 10000,m\le 100,k\le 10000000\)
我們先隨意選擇一個節點作為根節點 \(\mathit{rt}\),所有完全位於其子樹中的路徑可以分為兩種,一種是經過當前根節點的路徑,一種是不經過當前根節點的路徑。對於經過當前根節點的路徑,又可以分為兩種,一種是以根節點為一個端點的路徑,另一種是兩個端點都不為根節點的路徑。而後者又可以由兩條屬於前者鏈合併得到。所以,對於枚舉的根節點 \(rt\),我們先計算在其子樹中且經過該節點的路徑對答案的貢獻,再遞歸其子樹對不經過該節點的路徑進行求解。
在本題中,對於經過根節點 \(\mathit{rt}\) 的路徑,我們先枚舉其所有子節點 \(\mathit{ch}\),以 \(\mathit{ch}\) 為根計算 \(\mathit{ch}\) 子樹中所有節點到 \(\mathit{rt}\) 的距離。記節點 \(i\) 到當前根節點 \(rt\) 的距離為 \(\mathit{dist}_i\),\(\mathit{tf}_{d}\) 表示之前處理過的子樹中是否存在一個節點 \(v\) 使得 \(\mathit{dist}_v=d\)。若一個詢問的 \(k\) 滿足 \(tf_{k-\mathit{dist}_i}=true\),則存在一條長度為 \(k\) 的路徑。在計算完 \(\mathit{ch}\) 子樹中所連的邊能否成為答案後,我們將這些新的距離加入 \(\mathit{tf}\) 數組中。
注意在清空 \(\mathit{tf}\) 數組的時候不能直接用 memset,而應將之前佔用過的 \(\mathit{tf}\) 位置加入一個隊列中,進行清空,這樣才能保證時間複雜度。
點分治過程中,每一層的所有遞歸過程合計對每個點處理一次,假設共遞歸 \(h\) 層,則總時間複雜度為 \(O(hn)\)。
若我們 每次選擇子樹的重心作為根節點,可以保證遞歸層數最少,時間複雜度為 \(O(n\log n)\)。
請注意在重新選擇根節點之後一定要重新計算子樹的大小,否則一點看似微小的改動就可能會使時間複雜度錯誤或正確性難以保證。
參考代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
例題 2 Luogu P4178 Tree
給定一棵有 \(n\) 個點的帶權樹,給出 \(k\),詢問樹上距離小於等於 \(k\) 的點對數量。
\(n\le 40000,k\le 20000,w_i\le 1000\)
由於這裏查詢的是樹上距離為 \([0,k]\) 的點對數量,所以我們用線段樹來支持維護和查詢。
參考代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
例題 3 Luogu P2664 樹上游戲
一棵每個節點都給定顏色的樹,定義 \(s(i,j)\) 為 \(\mathit{i}\) 到 \(\mathit{j}\) 的顏色數量,\(\mathit{sum_{i}}=\sum_{j=1}^n s(i,j)\)。對所有的 \(1\leq i\leq n\),求 \(sum_i\)。(\(1 \le n, c_i \le 10^5\))
這道題很考驗對點分治思想的理解和應用,適合作為點分治的難度較高的例題和練習題。
首先,我們需要想明白一個轉化。題目定義 \(\mathit{sum_i}\) 是 \(i\) 到所有節點路徑上的顏色數量之和,可是如果用這個方法,在點分治中是不好統計答案的,因為這樣很難合併從當前根出發的兩棵子樹的信息。所以我們想到將 \(\mathit{sum_i}\) 的意義轉化。對於每個顏色 \(j\), 其中一個端點為 \(i\) 且含有顏色 \(j\) 的路徑數量記為 \(\mathit{cnt_j}\),\(\mathit{sum_i}\) 其實就是 \(\sum \mathit{cnt_j}\)。這一步轉化其實就是換了個觀察對象,考慮的是每個顏色對 \(\mathit{sum_i}\) 的 貢獻。而 \(\mathit{cnt_j}\) 其實很好處理出來,只需要每遇到一個新顏色,就 \(\mathit{cnt_{col_u}}+=\mathit{size_u}\) 即可,其中 \(\mathit{size_u}\) 為 u 的子樹大小,意味着這個子樹裏的所有節點都在這個顏色上對 \(u\) 的答案有一個貢獻。
考慮到點分治過程中,我們只需要分別考慮統計:
- 子樹中以當前根節點為端點的路徑對根的貢獻
- lca 為當前根節點的路徑對子樹內每個點的貢獻
1 部分比較好辦,由於點分治中,遞歸層數不超過 \(\log{n}\),每一層我們都可以遍歷全部子樹,這個時候就可以使用 \(\mathit{sum_i}\) 的定義式來在遍歷子樹的過程中順便統計了。
而針對 2 部分,設當前根節點 \(u\) 的一個子節點為 \(d\),\(d\) 的子樹裏任取一個點為 \(v\),那麼 \(v\) 的答案可以分為兩部分:
- \((u, v)\) 路徑上出現過的顏色,數量設為 \(\mathit{num}\),\(u\) 除了 \(d\) 以外的其他所有子樹的總大小設為 \(\mathit{siz1}\), 那麼這些出現過的顏色對 \(v\) 的答案貢獻為 \(\mathit{num}\times \mathit{siz1}\)。
- \((u, v)\) 路徑上沒有出現過的顏色 \(j\),它們的貢獻來自於 \(u\) 除了 \(d\) 以外的其他所有子樹的 \(\mathit{cnt_j}\),這部分答案為 \(\sum_{j \notin (u, v)} \mathit{cnt_j}\)。
以上是全部統計思路,實現細節詳見參考代碼。
參考代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
邊分治
與上面的點分治類似,我們選取一條邊,把樹儘量均勻地分成兩部分(使邊連接的兩個子樹的 \(\mathit{size}\) 儘量接近)。然後遞歸處理左右子樹,統計信息。
但是這是不行的,考慮一個菊花圖:
我們發現當一個點下有多個 \(\mathit{size}\) 接近的兒子時,應用邊分治的時間複雜度是無法接受的。
如果這個圖是個二叉樹,就可以避免上面菊花圖中應用邊分治的弊端。因此我們考慮把一個多叉樹轉化成二叉樹。
顯然,我們只需像線段樹那樣建樹就可以了。就像這樣
新建出來的點根據題目要求給予恰當的信息即可。例如:統計路徑長度時,將原邊邊權賦為 \(1\), 將新建的邊邊權賦為 \(0\) 即可。
分析複雜度,發現最多會增加 \(O(n)\) 個點,則總複雜度為 \(O(n\log n)\)
幾乎所有點分治的題邊分都能做(常數上有差距,但是不卡),所以就不放例題了。
點分樹
點分樹是通過更改原樹形態使樹的層數變為穩定 \(\log n\) 的一種重構樹。
常用於解決與樹原形態無關的帶修改問題。
算法分析
我們通過點分治每次找重心的方式來對原樹進行重構。
將每次找到的重心與上一層的重心締結父子關係,這樣就可以形成一棵 \(\log n\) 層的樹。
由於樹是 \(\log n\) 層的,很多原來並不對勁的暴力在點分樹上均有正確的複雜度。
代碼實現
有一個小技巧:每次用遞歸上一層的總大小 \(\mathit{tot}\) 減去上一層的點的重兒子大小,得到的就是這一層的總大小。這樣求重心就只需一次 DFS 了。
參考代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用