# 樹的路徑、直徑與重心 Path, Diameter and Centroid 要讀這篇以前請先讀過[樹的基本性質](https://hackmd.io/@ShanC/Trees-Basic-Properties),樹的專有名詞就不再解釋了。在前面的篇章,我們都是將樹作為一張連通圖的子圖來看待,進而去探討其中的性質。而這篇我們回到樹本身的性質,透過路徑、根節點與葉節點的關係,來探討一些樹上特別的節點 在實作上,這篇延續 [DFS 與 BFS](https://hackmd.io/@ShanC/bfs_dfs) 的變數名稱,有些東西就不解釋囉!! ## 樹上 DFS/BFS ### DFS on Tree 由於樹就是連通無環圖,因此在實作拜訪各節點的演算法時,可以省去布林陣列 `vis[]` 記錄是否拜訪。實作上只需要確保不要走到父節點即可 ```cpp void dfs(int u, int pre){ for(int &v : g[u]){ if(v != pre) dfs(v,u); } } ``` ### BFS on Tree 因為 BFS 很自然地就可以記錄起點到每個節點的距離,所以直接用它來判斷是否走過應該不為過吧! ```cpp void bfs(int s){ queue<int> q; q.push(s); vector<int> d(n + 1, -1); d[s] = 0; while(!q.empty()){ int u = q.front(); q.pop(); for(int &v : g[u]){ if(d[v] != -1) continue; d[v] = d[u] + 1; q.push(v); } } } ``` ## 樹的深度、高度與大小 Depth、Height and Size 給定一棵有根樹,根為 $r$。此樹有許多直觀的計數性質,以下來逐一說明並實作 小提醒 : 有時要注意題目是 $1$-based 還是 $0$-based ### 深度 Depth 深度就是與根的距離,超簡單!! ||所以就不附圖囉!|| #### BFS 實作 ```cpp void bfs(int r){ queue<int> q; q.push(r); vector<int> d(n + 1, -1); d[r] = 0; while(!q.empty()){ int u = q.front(); q.pop(); for(int &v : g[u]){ if(d[v] != -1) continue; d[v] = d[u] + 1; q.push(v); } } } ``` #### DFS 實作 ```cpp void dfs(int u, int pre){ for(int &v : g[u]){ if(v == pre) continue; d[v] = d[u] + 1; dfs(v, u); } } ``` ### 高度 Height 一個點的高度為所有子節點中,高度最高者$+1$ #### 舉例說明 如下圖,樹的高度就是根的高度,也就是 $3$ <center> <img src="https://hackmd.io/_uploads/Byko1TeOxl.png" style="margin: 0 auto; display: block; width: 400px"> </center> #### 程式實作 求樹高度要用到一點點樹上 DP 的觀念。這邊因為要保證子節點都已經處理完,因此 DFS 比較妥當 (當然你可以用拓樸排序,但是那很||白癡||) ```cpp void dfs(int u, int pre){ dep[u] = 0; for(int &v : g[u]){ if(v == pre) continue; dfs(v, u); dep[u] = max(dep[u], dep[v] + 1); } } ``` ### 大小 Size 這邊的大小是指子樹的節點數量,我們可以用一個式子來計算 : 令 $x$ 為任一個樹上的節點,$x$ 的孩子的集合 $\text{child}(x)$ $$ \text{size}(x)=1+\sum_{y\in \text{child(x)}} \text{size}(y) $$ 可以觀察到如果 $y$ 不是葉節點,那就要繼續再代入 $\text{size(y)}$,直到孩子集合為空才會停止。因此這是個遞迴式,可以用 DFS 來解 #### 範例 可以用下圖來算算看各個子樹的大小 <center> <img src="https://hackmd.io/_uploads/rJdYepldgl.png" style="margin: 0 auto; display: block; width: 400px"> </center> #### 程式實作 求子樹大小要用到一點點樹上 DP 的觀念。這邊因為要保證子節點都已經處理完,因此 DFS 比較妥當 (當然你可以用拓樸排序,但是那很||白癡||) ```cpp int dfs(int u, int pre){ sz[u] = 1; for(int &v : g[u]){ if(v == pre) continue; sz[u] += dfs(v, u); } return sz[u]; } ``` ## 直徑 Diameter ### 定義 #### 樹直徑 Diameter 給定一棵樹,則直徑為整棵樹最長的距離,即 $\displaystyle \max_{u, v\in V}\mathrm{dist}(u, v)$ #### 外心距 Eccentricity 對任意定點 $r$,定義其**外心距**(eccentricity)為 $\displaystyle e(r)\;=\;\max_{v\in V}\mathrm{dist}(r,v)$ ### 演算法 假設樹有 $n$ 個節點,樹存在 $g[~]$ 裡面。這個技巧叫做「換根」,直接給演算法,不解釋 ```cpp pair<int, int> bfs(int s){ int t = s; queue<int> q; vector<int> d(n + 1, -1); // 到 s 的距離 q.push(s); d[s] = 0; while(!q.empty()){ int u = q.front(); q.pop(); for(int &v : g[u]){ if(d[v] != -1) continue; d[v] = d[u] + 1; q.push(v); t = v; } } return {t, d[t]}; // {最遠葉子, 最遠距離} } int find_diameter(int r){ return bfs(bfs(r).first).second; } ``` ### 引理 1(外心距與直徑關係) 令 $D$ 為直徑,對任意樹上的節點 $r$,其外心距 $e(r)$ 滿足 : $$ \frac{D}{2} \;\le\; e(r) \;\le\; D $$ #### 證明 取一對直徑的端點 $(u,v)$,則 $\mathrm{dist}(u,v)=D$。由三角不等式,對任意 $r$: $$ \mathrm{dist}(u,v) \;\le\; \mathrm{dist}(u,r) + \mathrm{dist}(r,v) \;\le\; e(r) + e(r) = 2\,e(r) $$ 故 $e(r)\ge D/2$ 顯然對任意 $r$,$\displaystyle e(r)=\max_{v\in V}\mathrm{dist}(r,v)\le\max_{x,y\in V}\mathrm{dist}(x,y)=D$ ### 演算法正確性 下面給出證明,說明「從任意節點 $r$ 做一次 BFS 找到最遠節點 $a$,再從 $a$ 做一次 BFS,得到的最大距離即為樹的直徑」 #### 證明 #### 第一次 BFS : 從 $r$ 出發,得到外心距最大的節點 $a$,即 $\displaystyle d(r,a) = e(r) \;\ge\; \frac{D}{2}$ #### Claim : $a$ 必然是某條直徑路徑的端點 設 $(u,v)$ 是一對使 $\mathrm{dist}(u,v)=D$ 的直徑端點。由 $$ \mathrm{dist}(r,u) + \mathrm{dist}(r,v) \;\ge\; \mathrm{dist}(u,v) = D $$ 可推得 $$ \max\bigl(\mathrm{dist}(r,u),\,\mathrm{dist}(r,v)\bigr) \;\ge\; \frac{D}{2} $$ 而第一次 BFS 已選出使 $\mathrm{dist}(r,a)$ 最大的 $a$,且 $\mathrm{dist}(r,a)\ge D/2$。因此 $a$ 必為 $\{u,v\}$ 中之一 #### **第二次 BFS:** 既然 $a$ 是某條最長路徑 $(u,v)$ 的端點之一。那麼從 $a$ 出發,BFS 記錄到所有節點的距離,其中到另一端點 $b\in\{u,v\}\setminus\{a\}$ 的距離必為樹的直徑長度 : $$ \max_{x\in V}\mathrm{dist}(a,x) = \mathrm{dist}(a,b) = D $$ 因此第二次 BFS 回傳的最大距離正好是 $D$,證畢 ## 重心 Centroid ### 定義 1 以下我們採用演算法式操作的定義 : > 我們對所有節點 $x\in V$ 執行以下操作 $f(x)$ : > 1. 刪除節點 $x$,刪除後會出現森林 > 2. 統計每棵樹節點數,並記錄最大值 $m$ > 3. $f(x)$ 的值即為 $m$ > > 樹的重心為 $c$,$f(c)=\begin{split}\min_{x\in V}f(x)\end{split}$ ### 樹重心舉例說明 如下圖,$f(1)=\max\{4\}=4$、$f(2)=\max\{1, 3\}=3$、$f(3)=\max\{2, 1, 1\}=2$、$f(4)=\max\{4\}=4$、$f(5)=\max\{4\}=4$ 取最小之後,答案為 $3$,因為 $f(3)$ 最小 <center> <img src="https://hackmd.io/_uploads/SkPl29bOxx.png" style="margin: 0 auto; display: block; width: 400px"> </center> ### 根據定義 1 實作演算法 演算法有點複雜,因此我們分步驟討論 #### 說明 根據前面的定義,可以觀察到刪除節點 $x$ 會出現森林。我們觀察刪除節點後的結構,會發現有一棵原本是 $x$ 頭上的樹 (若 $x$ 是根就沒有),其他均為原本的子樹。如下圖,$f(x)=\max\{|T_a|, |T_b|, |T_c|\}=\max\{3, 2, 1\}=3$ 為最小,因此 $x$ 是重心 <center> <img src="https://hackmd.io/_uploads/rkSGQYW_eg.png" style="margin: 0 auto; display: block; width: 400px"> </center> 因此,要計算 $f(x)$ 我們不難得出以下步驟 : 1. 取所有子樹大小的最大值 $m'$ 2. $m'$ 跟頭上的節點比較得到 $m$ 接下來遍歷所有 $x\in V$ 就可以得到答案 #### 尋找子樹大小 這一步就跟[前面](#大小-Size)一樣 ```cpp int dfs(int u, int pre){ sz[u] = 1; for(int &v : g[u]){ if(v == pre) continue; sz[u] += dfs(v, u); } return sz[u]; } ``` #### 計算 $f(x)$ $x$ 頭上的樹的大小其實就是去掉 $x$ 為根的子樹後的大小,也就是 $\max\{m, n - sz[x]\}$,其中 $n$ 為整棵樹的節點數 ```cpp int f(int x, int pre){ int m = 0; for(int &child : g[x]){ if(child == pre) continue; m = max(m, sz[child]); } m = max(m, n - sz[x]); return m; } ``` #### DFS 遍歷 再次強調,因為這是拜訪整棵樹,所以可以省略 `vis[]`,只需要判斷是否為父節點即可 ```cpp int cent_sz = INF, cent; // cent_sz: 大小, cent: 重心 void find_centroid(int u, int pre){ int ans = f(u, pre); if(cent_sz > ans){ // 取最小值 cent_sz = ans; cent = u; } for(int &v : g[u]){ if(pre == v) continue; find_centroid(v, u); } } ``` #### 作法一 把上面材料組合在一起就可以得到 : ```cpp int n, cent_sz = INF, cent; int dfs(int u, int pre){ // 這個先預處理 sz[u] = 1; for(int &v : g[u]){ if(v == pre) continue; sz[u] += dfs(v, u); } return sz[u]; } int f(int x, int pre){ int m = 0; for(int &child : g[x]){ if(child == pre) continue; m = max(m, sz[child]); } m = max(m, n - sz[x]); return m; } void find_centroid(int u, int pre){ // 要找的時候呼叫這個 int ans = f(u, pre); if(cent_sz > ans){ cent_sz = ans; cent = u; } for(int &v : g[u]){ if(pre == v) continue; find_centroid(v, u); } } ``` ### 引理 1 若 $c$ 是重心,則將樹以 $c$ 為根,對任一子樹 $T_i$,其節點數 $|T_i|$ 都有 $|T_i| \;\le\; \bigl\lfloor\tfrac{n}{2}\bigr\rfloor$ <center> <img src="https://hackmd.io/_uploads/B110PWEdxx.png" style="margin: 0 auto; display: block; width: 400px"> </center> #### 證明 以下證明採用反證法 **假設** 假設存在一個子樹 $T_j$ 滿足 $|T_j| > \lfloor n/2\rfloor$ 那麼,刪除根節點 $c$ 後,這個連通塊大小為 $|T_j| > n/2$,所以 $$ f(c)\;\ge\; |T_j|\;>\;\frac{n}{2} $$ **觀察子節點** 取 $T_j$ 中緊鄰 $c$ 的那條邊上的節點,記作 $u$ 刪除節點 $u$ 時,原本與 $c$ 連通的「其餘 $n-|T_j|$ 個節點」會形成一個連通塊,其大小為 $$ n \;-\; |T_j|\;<\;n - \frac{n}{2} \;=\;\frac{n}{2} $$ 其他任何連通塊都屬於 $T_j$ 被切斷後的小塊,其大小必然小於 $|T_j|$,因此 $$ f(u)\;=\;\max\bigl(\,n-|T_j|,\;\text{「其他小連通塊」}\bigr)<\frac{n}{2}<f(c) $$ **得到矛盾** 根據重心定義,因 $c$ 是所有節點中 $f(\cdot)$ 最小的,所以 $$ f(c)\;\le\;f(u) $$ 但上面卻推得 $f(c)>f(u)$,矛盾 由反證法可知,不可能存在任何子樹大小超過 $\lfloor n/2\rfloor$。因此,若 $c$ 是重心,則以它為根時,每棵子樹的節點數都 $\le \lfloor n/2\rfloor$ **證畢** ### 引理 2 將樹以 $c$ 為根,若對任一子樹 $T_i$,其節點數 $|T_i|$ 都有 $|T_i| \;\le\; \bigl\lfloor\tfrac{n}{2}\bigr\rfloor$,則 $c$ 為重心 #### 證明 令樹的節點數為 $n$。把樹以 $c$ 為根,並假設對每一個子樹 $T_i$ 皆有 $$ |T_i|\le \left\lfloor\frac{n}{2}\right\rfloor $$ 我們要證對任意 $x\in V$ 有 $f(c)\le f(x)$ **(1) $f(c)$ 的上界** 刪掉 $c$ 後,森林剛好是這些子樹 $T_i$,因此 $$ f(c)=\max_{i\in I} |T_i| \le \left\lfloor\frac{n}{2}\right\rfloor\text{ , for some index } I $$ **(2) 對任意另一點 $u\neq c$,下界估計 $f(u)$** 考慮任意 $u\neq c$。在以 $c$ 為根的樹上,沿從 $c$ 到 $u$ 的路徑,設 $v$ 為這條路徑上緊鄰 $c$ 的第一個節點 (即 $v$ 是 $c$ 的某個 child,且 $u$ 在子樹 $T_v$ 內)。由假設,該子樹大小 $$ |T_v| \le \left\lfloor\frac{n}{2}\right\rfloor $$ 當刪去節點 $u$ 時,樹會被分成若干連通塊。其中有一個塊是**包含 $c$ 的連通塊**,這個連通塊至少包含整個樹中除了整個子樹 $T_v$ 外的所有節點。因此,刪掉 $u$ 後包含 $c$ 的那個連通塊的節點數至少是 $$ n - |T_v| $$ 因此 $$ f(u) \;\ge\; n - |T_v| \;\ge\; n - \left\lfloor\frac{n}{2}\right\rfloor \;=\; \left\lceil\frac{n}{2}\right\rceil $$ <center> <img src="https://hackmd.io/_uploads/SJQ_rGE_xg.png" style="margin: 0 auto; display: block; width: 400px"> </center> **(3) 比較 $f(u)$ 與 $f(c)$** 由 (1) 與 (2) 得到 : - $f(c) \le \lfloor n/2\rfloor$ - 對任意 $u\ne c$, $f(u) \ge \lceil n/2\rceil$ 注意到對所有整數 $n$ 有 $\lceil n/2\rceil \ge \lfloor n/2\rfloor$,因此對任意 $u\neq c$ : $$ f(u) \ge \lceil n/2\rceil \ge \lfloor n/2\rfloor \ge f(c) $$ 這表示 $f(c)$ 是所有 $f(x)$ 中的最小者,也就是 $c$ 是重心,證畢 ### 定理 1 將樹以 $c$ 為根,對任一子樹 $T_i$ 節點數 $|T_i|$ 都有 $|T_i| \;\le\; \bigl\lfloor\tfrac{n}{2}\bigr\rfloor$ 若且為若 $c$ 為重心 #### 證明 根據[引理 1](#引理-1) 與[引理 2](#引理-2),分別證明相反方向,得證 PS : 由於這個定理與原本的定義等價,因此也有人把這拿來當定義 ### 定理 2 從任意點 $x$ 沿著通往重心 $c$ 的方向移動,$f(x)$ 會 non-increasing,並在到達 $c$ 時達到最小值 #### 證明 其實不難觀察出這個結論,光是從[上面的例子](#樹重心舉例說明)就可以看得出這個性質。這部分就留給讀者自己練習 ### 根據定理 1 實作演算法 >將樹以 $c$ 為根,對任一子樹 $T_i$ 節點數 $|T_i|$ 都有 $|T_i| \;\le\; \bigl\lfloor\tfrac{n}{2}\bigr\rfloor$ 若且為若 $c$ 為重心 #### 作法二 有了上述的性質,我們可以得到另外一個版本的實作方式。每次找「刪去節點 $u$ 時,判斷是不是所有子樹都小於 $\bigl\lfloor\tfrac{n}{2}\bigr\rfloor$」。換句話說,可以直接去確認 $f(u)$ 是否小於 $\bigl\lfloor\tfrac{n}{2}\bigr\rfloor$,因為若最大的子都比 $\bigl\lfloor\tfrac{n}{2}\bigr\rfloor$ 小,那肯定所有的子樹都比 $\bigl\lfloor\tfrac{n}{2}\bigr\rfloor$ 小 這邊假設你已經學怎麼找子樹大小和上面的一坨東西,所以直接寫在同一個函式裡面 ```cpp int cent; // 這就是重心 void dfs(int u, int pre){ sz[u] = 1; int mx = 0; for(int &v : g[u]){ if(v == pre) continue; dfs(v, u); sz[u] += sz[v]; mx = max(mx, sz[v]); } mx = max(mx, n - sz[u]); if(mx <= n / 2) cent = u; } ``` #### 作法三 根據引理 2,令 $r$ 為根,則若存在 $|T_i| \ge\bigl\lfloor\tfrac{n}{2}\bigr\rfloor$,則重心存在於子樹 $T_i$,所以直接找這棵樹即可 <center> <img src="https://hackmd.io/_uploads/rk1J_GEugg.png" style="margin: 0 auto; display: block; width: 400px"> </center> `get_cent()` 會直接回傳答案。這樣子找,相當於從根節點移動到重心 ```cpp int dfs(int u, int pre){ // 這個先預處理 sz[u] = 1; for(int &v : g[u]){ if(v == pre) continue; sz[u] += dfs(v, u); } return sz[u]; } int get_cent(int u, int pre){ for(int &v : g[u]){ if(v != pre && sz[v] > n / 2) return get_cent(v, u); } return u; } dfs(1, 0); ans = get_cent(1, 0); ``` ### 時間複雜度 因為三個版本的作法都是 DFS,所以是 $O(n)$。要注意的是,因為處理的對象是樹,樹的邊數都是 $n-1$ 條邊,基本和節點數 $n$ 是同一個量級 值得注意的是,[作法三](#作法三)的 `get_cent()` 每次都挑最大的子樹而直接省去找其他子樹的時間,所以 `get_cent()` 本身是平均是 $O(\log n)$,但最差仍然是 $O(n)$。這對於需要一直做重心查詢的題目可以節省一點時間,像是以下的[重心剖分](#重心剖分-Centroid-Decomposition) <!-- ### 重心的距離性質 --> ## 重心剖分 Centroid Decomposition 因為重心有良好的[性質](#性質) : >若 $c$ 是重心,則將樹以 $c$ 為根,對任一子樹 $T_i$,其節點數 $|T_i|$ 都有 $|T_i| \;\le\; \bigl\lfloor\tfrac{n}{2}\bigr\rfloor$ 也就是說在刪除 $c$ 之後,所有樹的大小都會小於等於原本的一半,這個性質非常適合拿來做分治法 : 「每次找出重心後,將其刪除,樹會分割成森林。森林中的每棵樹又可以再找新的重心。」因為樹的重心會保證每棵子樹的大小都小於一半,因此分治法可以把時間壓到 $O(n\cdot \log n)$。這類型題目通常很實作,我都交給隊友,所以我自己通常也不太會解 這技巧就是一個 divide 的技巧,至於要怎麼 conquer 跟 combine 要看自己的實作技巧跟經驗 ### 演算法流程概述 這篇就提供一個大概的說明,因為這東西太實作了,每次寫出來都長不一樣 > 1. 找重心:一次 DFS 算子樹大小,再沿「大兒子」追下去 > 2. 處理經過重心的答案: > - 收集每個子樹中,以重心為源的距離分布 : `cnt[d]` 代表距離為 `d` 的數量 > - `cnt[0]=1` 表示重心本身 > - 先查詢再加入:取適當的 `cnt` 加入答案 > 3. 清理暫存:只清到本層用到的最大深度 > 4. 切掉重心,遞迴剩餘子樹 > 5. 停止條件:子樹空或單點 ### 複雜度分析 - 層數每次剖半 : $O(\log n)$ - 每層成本 : 找重心 $O(n)$、距離收集 $O(n)$ 總時間複雜度 : $O(n\cdot \log n)$ ### 常見題型 - **固定長度路徑 / 距離計數 (如「長度恰為 $k$ 的路徑數」)** 想法 : 距離陣列 `cnt[d]`,查詢後再加入 - **距離不超過 $k$ 的點對數** 想法 : 如上,改成前綴累加 - **條件限制 (顏色/屬性)** 想法 : `cnt` 變多維或分顏色維護;順序仍是「查詢 → 加入」 ### 例題說明 : [CSES Fixed-Length Paths I](https://cses.fi/problemset/task/2080) #### 題目 給一棵樹有 $n$ 個節點,求長度恰為 $k$ 的路徑數 #### 限制 - $1 \le k \le n \le 2 \cdot 10^5$ - $1 \le a,b \le n$ #### 題解 考慮重心剖分來將問題拆解成子問題,以下都是實作細節,沒什麼好說的 - `mx_dep` : 當前重心所用到的最大深度,用來限制清空 `cnt[]` 的範圍 - `cnt[d]` : 對「已處理過的子樹」中,距離當前重心為 `d` 的節點數;初始 `cnt[0]=1` - `cal_cnt()`: - 目的 : DFS 收集「從當前重心出發」至某子樹所有節點的距離 `d`,並依情境查詢 or 填表。 - 參數 : `u` : 目前 DFS 的節點 `pre` : 避免走回頭邊到父節點 `filling = false` : 查詢階段 `ans += cnt[k - dep]` `filling = true` : 填表階段 `cnt[dep]++` `dep`:從重心的鄰居開始,預設 `1` (重心本身視為距離 `0`,已由 `cnt[0]=1` 表示) - 剪枝 : 若 `dep > k`,則不可能再配對出長度恰為 `k` 的路徑 (因為距離只能增加),直接返回 - `decomp()`: 核心步驟 (針對包含 `u` 的當前子樹): 1. 用 `get_sz` 計算總大小;用 `get_cent` 找出重心 `cent` 2. 標記 `cent` 已移除 (當過重心),避免跨越到其他子樹 3. 針對 `cent` 的每個尚未被移除的子樹 : a. 先用 `cal_cnt(v, cent, false)`「查詢」貢獻 (避免同子樹內互配) b. 再用 `cal_cnt(v, cent, true)` 「填表」併入已處理子樹的距離陣列 4. 將本層用到的 `cnt[1..mx_dep]` 歸零,並維持 `cnt[0]=1`,留給下一層使用 5. 對每個未移除的鄰接子樹遞迴 `decomp()` ```cpp /* 以上略 */ const int MXN = 2e5 + 5; vector<int> g[MXN]; int n, k, mx_dep; ll ans = 0; bool vis[MXN]; int sz[MXN], cnt[MXN] = {1}; // 計算以 u 為根(忽略已移除的重心與父邊)的子樹大小 int get_sz(int u, int pre){ // 這個先預處理 sz[u] = 1; for(int &v : g[u]){ // 不跨越父親、也不進入已「移除當過重心」的節點 if(v == pre || vis[v]) continue; sz[u] += get_sz(v, u); } return sz[u]; } // 在包含 u 的當前子樹內找重心 : // 從 u 出發,若存在子樹大小 >= desired (約 n/2) 就往那個子樹走 // 直到找不到更大的子樹為止,回傳當前節點作為重心 int get_cent(int u, int pre, int desired){ for(int &v : g[u]){ if(v != pre && sz[v] >= desired && !vis[v]) return get_cent(v, u, desired); } return u; } void cal_cnt(int u, int pre, bool filling, int dep = 1){ if(dep > k) return; mx_dep = max(mx_dep, dep); // 記錄本層最大深度,供稍後清空 cnt[] 使用 if(filling) cnt[dep]++; // 填表 : 把這個子樹的距離分布,併入「已處理過的子樹」 else ans += cnt[k - dep]; // 查詢 : 找另一側距離是 (k - dep) 的節點數,配對成長度 k for(int &v : g[u]){ if(!vis[v] && pre != v) cal_cnt(v, u, filling, dep + 1); } } void decomp(int u = 1){ // 找重心 (若 total 偶數,可能落在任一個重心) int cent = get_cent(u, 0, get_sz(u, 0) / 2); vis[cent] = true; // 移除重心,切割成多個子樹 mx_dep = 0; // 重置本層使用過的最大深度 // 針對每個鄰接子樹 : 先查詢、後填表 (避免同一子樹內互相配對) for(int &v : g[cent]){ if(vis[v]) continue; cal_cnt(v, cent, false); // 查詢 : 累加所有「經過重心」的長度= k 的路徑 cal_cnt(v, cent, true); // 填表 : 把這個子樹的距離分布併入 cnt[] } // 重置 cnt[] : 保留 cnt[0]=1 (代表僅重心),清掉本層曾經動用過的深度範圍 // 註 : 此實作選擇在「離開當前重心」時做清理,確保下一層剛進來時 cnt 狀態就是正確的 cnt[0] = 1; for(int i = 1; i <= mx_dep; i++) cnt[i] = 0; // 遞迴處理每個子樹 (此時重心已被標記,不會跨越到其他分支) for(int &v : g[cent]){ if(!vis[v]) decomp(v); } } /* 以下略 */ ``` ## 題目練習 基本上在競程遇到這類問題都是要稍微腦袋轉一下,沒那麼直觀 [Zerojudge f673. FJCU_109_Winter_Day2_Lab1 樹高](https://zerojudge.tw/ShowProblem?problemid=f673) (裸題) [Zerojudge c463. apcs 樹狀圖分析 (Tree Analyses)](https://zerojudge.tw/ShowProblem?problemid=c463) (有點裸) [Zerojudge b218. 3. 找關鍵人物](https://zerojudge.tw/ShowProblem?problemid=b218) (子樹大小+算一算) [Zerojudge b967. 4. 血緣關係](https://zerojudge.tw/ShowProblem?problemid=b967) (裸題) [CSES Subordinates](https://cses.fi/problemset/task/1674/) (裸題) [CSES Tree Diameter](https://cses.fi/problemset/task/1131) (裸題) [CSES Tree Distances I](https://cses.fi/problemset/task/1132/) (直徑的性質,注意到每個點最遠距離會在直徑兩端點,證證看) [codeforces 1404B. Tree Tag](https://codeforces.com/contest/1404/problem/B) [codeforces 1294F. Three Paths on a Tree](https://codeforces.com/problemset/problem/1294/F) (已知直徑,怎麼求離直徑的最遠距離呢?) [codeforces 734E. Anton and Tree](https://codeforces.com/contest/734/problem/E) (DSU + 半徑) [codeforces 456E. Civilization](https://codeforces.com/contest/456/problem/E) (DSU + 如何構建最短直徑) [AtCoder Beginner Contest 221 F. Diameter set](https://atcoder.jp/contests/abc221/tasks/abc221_f?lang=en) [codeforces 761E. Christmas Chocolates](https://codeforces.com/contest/1617/problem/E) [codeforces 1214H. Tiles Placement](https://codeforces.com/contest/1214/problem/H) [codeforces 1406C. Link Cut Centroids](https://codeforces.com/contest/1406/problem/C) (這題可以學到關於重心唯一的條件) [CSES Finding a Centroid](https://cses.fi/problemset/task/2079) (裸題,可以用這題試試看三種找重心的方法) [codeforces 685B. Kay and Snowflake](https://codeforces.com/problemset/problem/685/B) (半裸題,需要用到重心的定理 2) [codeforces 321C. Ciel the Commander](https://codeforces.com/contest/321/problem/C) [CSES Fixed-Length Paths I](https://cses.fi/problemset/task/2080) (裸的重心剖分) [CSES Fixed-Length Paths II](https://cses.fi/problemset/task/2081) (相信我,BIT會TLE得很慘,update跟query有一堆log) [codeforces 776F. Sherlock's bet to Moriarty](https://codeforces.com/contest/776/problem/F) [codeforces 342E. Xenia and Tree](https://codeforces.com/problemset/problem/342/E) ---- ## 參考資料 - [[Tutorial] Diameter of a tree and its applications](https://codeforces.com/blog/entry/101271) - [Proof of algorithm to find the diameter of a tree.](https://codeforces.com/blog/entry/60440) - [Centers of a tree](https://www.tutorialspoint.com/centers-of-a-tree) - [Centroid Decomposition of Tree](https://www.geeksforgeeks.org/dsa/centroid-decomposition-of-tree/) - [海大競程 - 圖上遍歷 & 樹論](https://hackmd.io/@LeeShoWhaodian/I2CP2024tree1#/) - [台師大演算法筆記 - tree](https://web.ntnu.edu.tw/~algo/Tree.html) - [USACO - Centroid Decomposition](https://usaco.guide/plat/centroid) - [OI Wiki - 樹的重心](https://oi-wiki.org/graph/tree-centroid/) ---- > [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC) > 作者: ShanC > 更新: 2025/8/10