# 樹的路徑、直徑與重心 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