---
# System prepended metadata

title: 最低共同祖先 Lowest Common Ancestor
tags: [內湖高中程式競賽]

---

# 最低共同祖先 Lowest Common Ancestor
前面我們講過[倍增法](https://hackmd.io/@ShanC/Binary-Exponentiation-and-Lifting#%E5%80%8D%E5%A2%9E%E6%B3%95%E7%B0%A1%E4%BB%8B)，這篇我們會使用這個方法來實作最低共同祖先。但話說回來，什麼是最低共同祖先呢? 本篇會來好好說明這個東西。接下來會來討論，這個東西可以拿來做什麼。最低共同祖先可以方便我們在對樹上整條路徑做操作，是個好用的工具

## 最低共同祖先 Lowest Common Ancestor
### 複習一些名詞
- 路徑 : 一個節點序列 (有時會看作是一個集合，要看前後文)，之間不會有重複的節點。通常以兩端點表示，如 $a, b\text{-path}$
- 葉節點 : 樹上度數為 $1$ 的節點，每棵樹上一定會有 $2$ 以上片葉子
- 根節點 : 與葉不同，這是一個需要「人為定義」的節點。每個節點 (包含葉) 都可以當作根，而一棵樹只會有一個根。通常寫作 $r$
- 有根樹 : 擁有根節點的樹。**在這篇都是討論有根樹，所以會省略一棵樹是有根樹這件事**
- $x$ 的祖先 : 在 $r, x\text{-path}$ 上，比節點 $x$ 更靠近 $r$ 的節點們 (可能不只一個)

### 共同祖先 Common Ancestors
令有根樹的根為 $r$，對於兩個節點 $x,y$，共同祖先 $\text{CA}(x, y)$ 為 $r,x\text{-path }$ 與 $r,y\text{-path}$ 共同的節點

### 最低共同祖先 Lowest Common Ancestor
令有根樹的根為 $r$，對於兩個節點 $x,y$，最低共同祖先是在 $\text{CA} (x, y)$ 中離 $x$ 與 $y$ 最近的節點 $\text{LCA}(x, y)$。要注意的是，因為在樹當中路徑是唯一的，所以 $\text{LCA}$ 也是唯一

### 舉例說明
由下圖可知，$\text{LCA}(a, b)=x$、$\text{CA}(a, b) = \{x, y\}$
<center>
<img src="https://hackmd.io/_uploads/SJWhAQvdge.png" style="margin: 0 auto; display: block;  width: 400px">
</center>

### 樹深度
每個節點的深度就是與根的距離，我們在 DFS/BFS 時就可以順便算出來
```cpp
int dep[MXN];
void dfs_dep(int u, int pre){
    for(int &v : g[u]){
        if(v == pre) continue;
        dep[v] = dep[u] + 1;
        dfs_dep(v, u);
    }
}
```

## 複習倍增法
還記得[倍增法](https://hackmd.io/@ShanC/Binary-Exponentiation-and-Lifting)嗎? 倍增法會把每個節點 $x$ 的 $2^i$ 代祖先都記在 `anc[x][i]` 裡面，細節請自己去複習之前寫的那篇


### 輸入
有時候我們可以在輸入時就把 `anc[][]` 每個節點父親的部分先建好
```cpp
int anc[MXN][logN];           // MXN: 最大節點數、logN: 最大祖先數
cin >> n;                     // 輸入節點數量
for (int i = 2; i <= n; i++){
    cin >> father >> x;       // 輸入 x 的父節點、x
    anc[x][0] = father;       // 父節點就是 2^0 倍祖先
}
```

### 初始化
這邊就是把「倍增表」建好，使用的是 DP 的技巧
```cpp
// logN: 最大的祖先數
for (int i = 1; i < logN; i++) {  // binary lifting
    for (int u = 1; u <= n; u++) {
        anc[u][i] = anc[anc[u][i - 1]][i - 1];
    }
}
```

### 爬 $k$ 步
有些人會把這步寫成 $k\text{-th ancestor}(x, k)$，簡單來說就是從 $x$ 爬到第 $k$ 代祖先
```cpp
int move(int x, int k){
    for (int i = 0; i < logN; i++) {
        if (k & (1 << i))
            x = anc[x][i];
    }
    return x; // 這裡要注意，若移動超過根，就會回傳 0
}
```


### 注意到
- $r$ 是樹上所有節點的共同祖先
- $x$ 與 $y$ 的共同祖先能在 `anc` 上找到。若 $x$ 與 $y$ 的深度相同，那麼他們同時往上走，會在對低共同祖先碰在一起

如[上圖](#舉例說明)，$y$ 是大家的共同祖先；$a$ 與 $b$ 同時往上走一步會到 $\text{LCA}(a, b)=x$

## 複習時間戳記
時間戳記是 DFS 本身就會有的特性之一，你應該要知道才對，因為在最基礎的[這篇](https://hackmd.io/@ShanC/bfs_dfs#%E6%99%82%E9%96%93%E6%88%B3%E8%A8%98)講過了

### 程式碼實作
我們可以在上述遞迴版本的程式碼中加入: 
- $d[i]$: 發現節點 $i$ 的時間 (discovery time)
- $f[i]$ : 節點 $i$ 完成所有鄰居拜訪的時間 (finishing time)

```cpp
void dfs_time(int u, int pre) {
    d[u] = ++timestamp;  // 紀錄發現時間
    for (int &v : g[u]) {
        if (v != pre) dfs_time(v, u);
    }
    f[u] = ++timestamp;  // 紀錄結束時間
}
```

我們把[上圖](#舉例說明)跟 $d[i]$、$f[i]$ 結合起來可以得到下圖。我們可以把時間戳記當作是一個計時器

<center>
<img src="https://hackmd.io/_uploads/ByPVHVv_xg.png" style="margin: 0 auto; display: block;  width: 600px">
</center>


### 括號定理

當我們把[上圖](#舉例說明)所有節點 $v$ 的發現與結束時間畫在數線上，會形成好幾個區間

<center>
<img src="https://hackmd.io/_uploads/Bkq8VG__gx.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

藉由上圖會發現，對於任兩節點 $u$ 與 $v$ 在樹中 : 
1.  如果區間 $[d[u], f[u]]$ 包含 $[d[v], f[v]]$，則 $u$ 為 $v$ 的祖先
2.  如果區間 $[d[v], f[v]]$ 包含 $[d[u], f[u]]$，則 $u$ 為 $v$ 的後代
3.  如果區間 $[d[u], f[u]]$ 與 $[d[v], f[v]]$ 沒有交集，則 $u$ 與 $v$ 沒有血緣關係

可以得到推論 : 在一棵樹中，$d[u]<d[v]<f[v]<f[u]$ 若且唯若，$v$ 是 $u$ 的後代

## 如何找最低共同祖先?
### 括號定理確定是否為祖先
根據[括號定理的推論](#括號定理)，我們可以知道該如何查詢任一個節點 $u$ 是不是節點 $v$ 的祖先。由於對於任一點 $x$，$d[x]<f[x]$ 都是確定的，因此只需要確認 $d[u] < d[v]$ 與 $f[v] < f[u]$ 就好。值得注意的是，根據程式碼，若 $d[u]=d[v]$ 那 $f[u]=f[v]$，也就代表 $u=v$。因為在 $u=v$ 的情況下**有時候**也算是彼此的祖先，因此可以將 $<$ 替換成 $\leq$

```cpp
bool is_ancestor(int u, int v){
    return d[u] <= d[v] && f[v] <= f[u];
}
```

### 粗暴地往根的方向找

有了確認是否為祖先的方式，那我們就可以來處理找最低共同祖先的問題。給定兩點 $u, v$，我們可以考慮以下方法 : 
1. $u$ 往上走，直到 $u$ 成為 $v$ 的祖先
2. $u$ 的位置即為 $\text{LCA}(u, v)$

但是這方法可能會花很多時間，每次最差都要花費 $O(n)$ 的時間

### 引入倍增法
根據[上面](#粗暴地往根的方向找)的想法，我們引入倍增法。實作很簡單，但要理解就很難，程式碼只可意會不可言傳，以下提供給讀者自行體會

```cpp
int getlca(int u, int v){
    if (is_ancestor(u, v)) return u;     // 如果 u 為 v 的祖先則 lca 為 u
    if (is_ancestor(v, u)) return v;     // 如果 v 為 u 的祖先則 lca 為 u
    for (int i = logN; i >= 0; i--){     // 判斷 2^logN, 2^(logN-1),...2^1, 2^0 倍祖先
        if (anc[u][i] && 
            !is_ancestor(anc[u][i], v))  // 如果 2^i 倍祖先不是 v 的祖先
            u = anc[u][i];               // 則往上移動
    }
    return anc[u][0];                    // 回傳此點的父節點即為答案
}
```

每一次查詢都是 $O(\log n)$ 的時間，比原本 $O(n)$ 好很多

### 直接利用倍增法來找

[上面](#引入倍增法)的方法需要預先跑一次時間戳記的程式才能知道開始與結束時間，進而用來判斷 $u$ 是否為 $v$ 的祖先。事實上我們不需要這麼麻煩。我們原本的目的只是想要找最低共同祖先而已，而我們在[前面已經注意到](#注意到)以下兩點 : 
1. 若 $u$ 與 $v$ 的深度相同，則移動相同步數可以同時抵達 $\text{LCA}$
2. 若 $u$ 的深度比 $v$ 深，則可以將 $u$ 移動到相同深度後，回到 1. 的情況

```cpp
int getlca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);      // 判斷誰深度最深
    u = move(u, dep[u] - dep[v]);    // 爬到相同深度
    if(u == v) return u;                 // 如果兩者在相同位置那調表那就是 LCA
    for(int i = logN; i >= 0; i--){      // i 從大到小
        if(anc[u][i] != anc[v][i]){      // 如果兩者爬 2^i 步仍不相同
            u = anc[u][i];               // 倍增法爬到 2^i 代祖先
            v = anc[v][i];
        }
    }
    return anc[u][0];                    // 父親就是 LCA
}
```

## 程式實作
上面我們寫得太亂了，以下我把它整理成一份模板。由於陣列初始化都為 $0$，所以宣告就不寫了。以下的程式碼與[上面](#輸入)不同，這裡先假設 `anc[][]` 還沒建好
### 方法一 : 用時間戳記判斷其中一點是否為另一點的 LCA
找 LCA 在競程上通常是線上算法，因此最前置作業就是要先初始化 `anc[][]` 與時間戳記 :

1. 先 DFS 一次，把時間戳記與 `anc` 中每個節點的 $2^0$ 代祖先建立好
2. 把倍增表 `anc[][]` 建好

接下來就是呼叫 `getlca()` 請求中，每次要做的事情 : 
1. 判斷兩節點 $u$ 是否為 $v$ 的祖先，或著反之 $v$ 是否為 $u$ 的祖先。是的話直接回傳即可 $O(1)$
(這裡有個實作細節，若題目說同一點互為彼此的祖先就要從 $<$ 改成 $\leq$)
2. 接下來，程式移動其中一節點 $u$ (用倍增法從跳比較遠到跳比較近的距離)，判斷他父親是不是 $\text{LCA}$
    - 否的話就往上爬
    - 是則忽略 (因為如果爬的話有可能超過)
3. 回傳 $u$ 的父親即為 $\text{LCA}$

```cpp
void dfs_time(int u, int pre) {          // 時間戳記
    d[u] = ++timestamp;                  // 紀錄發現時間
    anc[u][0] = pre;                     // 這裡就順便紀錄爸爸是誰
    for (int &v : g[u]) {
        if (v != pre) dfs_time(v, u);
    }
    f[u] = ++timestamp;                  // 紀錄結束時間
}

void init(){                             // 先執行這個
    dfs_time(1, 0);                      // 假設 1 為根
    for (int i = 1; i < logN; i++) {     // binary lifting
        for (int u = 1; u <= n; u++) 
            anc[u][i] = anc[anc[u][i - 1]][i - 1];
    }
}

bool is_ancestor(int u, int v){          // 判斷是否為祖先
    return d[u] <= d[v] && f[v] <= f[u]; // 實作細節 : 有可能會遇到同一點的狀況，所以要加 =
}

int getlca(int u, int v){
    if (is_ancestor(u, v)) return u;     // 如果 u 為 v 的祖先則 lca 為 u
    if (is_ancestor(v, u)) return v;     // 如果 v 為 u 的祖先則 lca 為 u
    for (int i = logN; i >= 0; i--){     // 判斷 2^logN, 2^(logN-1),...2^1, 2^0 倍祖先
        if (anc[u][i] && 
            !is_ancestor(anc[u][i], v))  // 如果 2^i 倍祖先不是 v 的祖先
            u = anc[u][i];               // 則往上移動
    }
    return anc[u][0];                    // 回傳此點的父節點即為答案
}
```
### 方法二 : 先到相同深度再找 LCA
這個方法就不需要時間戳記了，不要忘了時間戳記需要兩個序列 $d[~]$ 與 $f[~]$ 來維護，而這種方法只須要維護一個深度的陣列 `dep[]` 即可，省一點常數空間的同時還可以重複利用 `dep[]` 來計算其他事情，像是[以下就是很好的例子](#LCA-應用-:-求樹上任意兩點距離)。這方法的初始化詳細步驟如下 : 

1. 跑 DFS，把 `dep[]` 與 `anc` 中每個節點的 $2^0$ 代祖先建立好
2. 把倍增表 `anc[][]` 建好

接下來在 `getlca()` 中 : 
1. 先將比較深的節點設為 $u$，淺的設為 $v$
2. 將比較深的節點 $u$ 移到跟 $v$ 相同的高度
3. 若兩節點相同則直接回傳
4. 用迴圈跳倍增法，從遠到近跳 : 
    - 若兩者的第 $2^i$ 代祖先不相同，則兩節點分別跳到該代祖先
    - 否則忽略 (不然會跳超過 $\text{LCA}$)
5. 兩節點的父親即為 $\text{LCA}$，回傳答案


```cpp
void dfs_dep(int u, int pre) {           // 求深度
    anc[u][0] = pre;                     // 這裡就順便紀錄每個節點爸爸是誰
    for (int &v : g[u]) {
        if (v == pre)
            continue;
        dep[v] = dep[u] + 1;
        dfs_dep(v, u);
    }
}

void init() {                            // 先執行這個
    dfs_dep(1, 0);                       // 假設 1 為根
    for (int i = 1; i < logN; i++) {     // binary lifting
        for (int u = 1; u <= n; u++)
            anc[u][i] = anc[anc[u][i - 1]][i - 1];
    }
}

int move(int x, int k){                 // 倍增法往上移動 k 步
    for (int i = 0; i < logN; i++) {
        if (k & (1 << i))
            x = anc[x][i];
    }
    return x;                            // 這裡要注意，若移動超過根，就會回傳 0
}

int getlca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);      // 判斷誰深度最深
    u = move(u, dep[u] - dep[v]);        // 爬到相同深度
    if(u == v) return u;                 // 如果兩者在相同位置那調表那就是 LCA
    for(int i = logN; i >= 0; i--){      // i 從大到小
        if(anc[u][i] != anc[v][i]){      // 如果兩者爬 2^i 步仍不相同
            u = anc[u][i];               // 倍增法爬到 2^i 代祖先
            v = anc[v][i];
        }
    }
    return anc[u][0];                    // 父親就是 LCA
}
```

## LCA 應用 : 求樹上任意兩點距離
在之前的章節，要找到圖中兩點距離，可以用 BFS 以其中一點為起點開始遍歷。若今天的情境是每次都要詢問樹上任兩點的距離，那這樣子太慢了
### 推導
假設我們已知深度，設 $\mathrm{lca}=\text{LCA}(a, b)$，即任一點至根的距離 $\mathrm{dist}(r, x)$。若要求兩點距離 $\mathrm{dist}(a, b)$，那麼我們可以推出 : 
$$
\begin{split}
\mathrm{dist}(a, b)&=\mathrm{dist}(a, \mathrm{lca}) + \mathrm{dist}(\mathrm{lca}, b)\\
&=(\mathrm{dist}(a, r)-\mathrm{dist}(r, \mathrm{lca}))+(\mathrm{dist}(b, r)-\mathrm{dist}(r, \mathrm{lca}))\\
&=(\mathrm{depth}(a)-\mathrm{depth}(\mathrm{lca}))+(\mathrm{depth}(b)-\mathrm{depth}(\mathrm{lca}))\\
&=\mathrm{depth}(a)+\mathrm{depth}(b)-2\cdot\mathrm{depth}(\mathrm{lca})
\end{split}
$$

### 程式碼實作
```cpp
int dist(int a, int b){
    return dep[a] + dep[b] - 2 * dep[getlca(a, b)];
}
```

## LCA 應用 : 樹上差分
大多時候我們在競程的前綴和、後綴和、差分都是在有限的非負整數，但其實我們可以把這種概念抽象化變成一條鏈，每次我們做前綴和、後綴和、差分等操作都是在鏈上。眾所周知，鏈是一種圖。所以我們可以很輕鬆地把前綴和、差分等概念拓展到樹上。我們可以在樹上前綴和、後綴和、差分，因為 : 
- 樹上所有點對的路徑都是唯一
- 每個節點的父節點都是唯一

### 樹上前綴和
可以想像，樹就是一條有很多分支的數軸，共通點是都有一個起點叫做根。若賦予每個節點一個權重，我們可以從根節點出發，並且把父節點的值都加進子節點的值。如此一來就可以得到數上前綴和
```cpp
void dfs(int u, int pre){
    for(int &v : g[u]){
        if(pre == v) continue;
        pre[v] += pre[u]; // 樹上前綴和
        dfs(v, u);
    }
}
```

樹上前綴和的效果大概就如下圖所示
<center>
<img src="https://hackmd.io/_uploads/r1tJmOuOxx.png" style="margin: 0 auto; display: block;  width: 600px">
</center>


### 樹上後綴和
從某節點 $u$，一路把值加回到根節點。這執行的順序跟拓樸排序很像
```cpp
void dfs(int u, int pre){
    for(int &v : g[u]){   // 樹上每個 v 的父節點都唯一
        if(pre == v) continue;
        dfs(v, u);
        suf[u] += suf[v]; // 樹上後綴和
    }
}
```

樹上後綴和的效果大概就如下圖所示
<center>
<img src="https://hackmd.io/_uploads/H1lkEdu_gx.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

因數上每個節點都只有一個父親，樹上後綴和的效果只會作用在一條通往根的路徑上

註 : 「樹上後綴和」是我取的名字，為了與樹上前綴和做出區別，不知道為什麼網路上沒人為這個技巧命名，頂多把他歸類在 DP on tree

### 樹上差分
在樹上前綴和，每次都會把權重加到整顆子樹的節點上；樹上後綴和，每次都把權重加到點到根的路徑上的所有節點。但是有時候我們只想要加權重 $w$ 在某條特定 $a, b\text{-path}$ 的節點上，該怎麼辦呢? 

前綴和我們不考慮。若對 $a, b$ 分別加值並使用樹上後綴和，那麼會發現以下現象 : $\text{LCA}(a, b)$ 到 $r$ 的路徑上會同時被 $a$ 與 $b$ 的權重加值

<center>
<img src="https://hackmd.io/_uploads/BJXqvuu_xl.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

這問題不難解決，多出來的值，減掉就好。不難觀察到，$\text{LCA}(a, b)$ 被多加了一次，其餘從$\text{LCA}(a, b)$ 的父節點到 $r$ 則是被多加兩次。我們可以考慮樹上差分技巧 : 在路徑的兩端點 $a, b$ 加值後，$\text{LCA}(a, b)$ 與 $\text{LCA}(a, b)$ 的父節點各減一次值。如此一來就會對 $\text{LCA}(a, b)$ 到 $r$ 的路徑毫無影響

<center>
<img src="https://hackmd.io/_uploads/BkeRtdd_eg.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

因此，若我們想只加權重在某條路徑上，在加值時使用以下這個方法即可
```cpp
void add(int a, int b, ll w){
    int lca = getlca(a, b); // 求 LCA(a, b)
    suf[a] += w;            // a 加值
    suf[b] += w;            // b 加值
    suf[lca] -= w;          // lca 減值
    suf[anc[lca][0]] -= w;  // lca 的父節點減值
}
```

值得注意的是，樹上差分與序列上差分最不同的是，樹上差分是離線算法，無法一邊輸入一邊處理資料，因為時間會炸掉

## LCA 應用 : 次小生成樹 Second Best Minimum Spanning Tree
最小生成樹在[這篇](https://hackmd.io/@ShanC/minimum-spanning-tree)講過了，所以就不前情提要囉!!

### 次小生成樹
次小生成樹簡稱 SBMST。簡單來說就是權重總和第二小的生成樹，沒什麼大不了的。假設我們已經用 Kruskal 演算法求出最小生成樹 $T$，我們要找出一條邊 $e'\notin T$ 與另一條邊 $e\in T$，使得 $T'=(T\setminus\{e\})\cup\{e'\}$ 是一棵生成樹且 $w(e')-w(e)$ 為最小。$w(T')$ 是次小生成樹的權重和

### 爛方法 : 窮舉
已知最小生成樹有 $n - 1$ 條邊，非樹邊有 $O(m)$ 條。若是窮舉兩者，我們就可以找出最小的差值。已知每條非樹邊都是由樹上兩個節點組成，在樹上也一定有路徑可以通往彼此。為了要保證圖是連通的，我們必須先窮舉非樹邊，邊上兩節點找到對應的樹上路徑來走訪，並拔掉樹上路徑最大邊權的邊。時間複雜度 $O(nm)$

<center>
<img src="https://hackmd.io/_uploads/SJ7weMj_xx.png" style="margin: 0 auto; display: block;  width: 600px">
</center>


### 簡化生成樹上的搜尋
我們知道，若使用倍增法可以簡化兩點之間的詢問，是個很好的方法。可以考慮建立在此基礎上，用倍增法的方式維護「每個節點到 $2^i$ 代祖先的邊權最大值」

為了達成此目的，我們必須先建樹。我們直接在 Kruskal 演算法的片段插入 `g[]`，非樹邊的查詢也可以在這時候完成

```cpp
vector<Edge> notree;
for (int i = 0; i < m; i++) {
    if (dsu.find(edge[i].u) != dsu.find(edge[i].v)) { // 如果兩點未聯通
        dsu.merge(edge[i].u, edge[i].v); // 將兩點設成同一個集合
        mst_val += edge[i].w;  // 權重加進答案
        g[edge[i].u].push_back({edge[i].v, edge[i].w});
        g[edge[i].v].push_back({edge[i].u, edge[i].w});
    }
    else{
        notree.push_back(edge[i]);
    }
}
```

做好之後，我們可以開始建立生成樹了。這邊我們再多維護一個陣列 `mxcost[]` 維護「每個節點到 $2^i$ 代祖先的邊權最大值」
```cpp
void dfs_dep(int u, int pre) {           
    anc[u][0] = pre;
    for (auto [v, w] : g[u]) {
        if (v == pre) continue;
        mxcost[v][0] = w; // 這裡紀錄一條邊的權重
        dep[v] = dep[u] + 1;
        dfs_dep(v, u);
    }
}

void init() {  
    dfs_dep(1, 0);
    for (int i = 1; i < logN; i++) {
        for (int u = 1; u <= n; u++){
            anc[u][i] = anc[anc[u][i - 1]][i - 1];
            mxcost[u][i] = max(mxcost[u][i - 1], mxcost[anc[u][i - 1]][i - 1]);
        }
    }
}
```

最後在查詢的時候使用 `ret` 紀錄答案
```cpp
int getlca_val(int u, int v){ // 這邊用方法二來實作
    if(dep[u] < dep[v]) swap(u, v);
    
    ll ret = 0;
    int k = dep[u] - dep[v];
    for (int i = 0; i < logN; i++) {
        if (k & (1 << i)){
            ret = max(ret, mxcost[u][i]);
            u = anc[u][i];
        }
    }
      
    if(u == v) return ret;
    for(int i = logN; i >= 0; i--){
        if(anc[u][i] != anc[v][i]){
            ret = max({ret, mxcost[u][i], mxcost[v][i]});
            u = anc[u][i];
            v = anc[v][i];
        }
    }
    ret = max({ret, mxcost[u][0], mxcost[v][0]});
    return ret; 
}
```

### 查詢次小生成樹權重
最後在窮舉非樹邊的時候，直接加加減減就好了
```cpp
ll sbmst = INF;
for(int i = 0; i < notree.size(); i++){
    int lca_val = getlca_val(notree[i].u, notree[i].v);
    sbmst = min(sbmst, mst_val + notree[i].w - lca_val);
}
cout << sbmst << '\n';
```
如此一來就能求出次小生成樹的權重和囉!!

## 可能採雷的點
因為筆記也不可能涵蓋所有狀況，所以出問題的時候別怪我。大多 $\text{LCA}$ 會出的問題都放下面

### 實作上
- `logN` 沒定好，導致 **<font color="EEFF23">RE</font>**。建議可以將 `anc[][]` 陣列開大一點
- 倍增法爬到超過根節點 : 可能要加個 `if (anc[u][i])` 作為防護
- 根是 $0$ 還是 $1$ 還是其他? 這些實作細節沒注意到可能都會 **<font color="EEFF23">RE</font>**
- 有時候題目可能會是森林，會有多個根，需要注意一下

### 解題上
有時會誤以為 $\text{LCA}$、倍增法與時間戳記可以解決，但實際上測試時卻覺得行不通。這時你可以嘗試重心剖分、輕重樹鏈剖分或是嘗試引入 RMQ 技巧

### 溝通上
跟非競程人士溝通時，一定要跟別人說清楚你的 $\text{LCA}$ 是指「最低共同祖先」，不然別人會以為你的肚子不舒服
<center>
<img src="https://hackmd.io/_uploads/Ski9_PF_le.jpg" style="margin: 0 auto; display: block;  width: 600px">
</center>


## 題目練習
[AtCoder Beginner Contest 138D - Ki](https://atcoder.jp/contests/abc138/tasks/abc138_d) (樹上前綴和裸題)
[AtCoder Beginner Contest 209D. Collision](https://atcoder.jp/contests/abc209/tasks/abc209_d) (樹上任意兩節點距離裸題)
[Zerojudge d767. 血緣關係](https://zerojudge.tw/ShowProblem?problemid=d767) (樹上任意兩節點距離裸題)
[CSES Company Queries II](https://cses.fi/problemset/task/1688/) (求 LCA 裸題)
[CSES Distance Queries](https://cses.fi/problemset/task/1135/) (樹上任意兩節點距離裸題)
[CSES Counting Paths](https://cses.fi/problemset/task/1136/) (樹上差分裸題)
[Codeforces 208E. Blood Cousins](https://codeforces.com/contest/208/problem/E) (動動腦，請善用時間戳記與一些 STL，不用找 LCA)
[Codeforces 609E. Minimum spanning tree for each edge](https://codeforces.com/problemset/problem/609/E) (次小生成樹半裸題)
[CSES MST Edge Check](https://cses.fi/problemset/task/3407) (應該跟上面一模一樣)
[Zerojudge c495. 四、次小生成樹(SBMST)](https://zerojudge.tw/ShowProblem?problemid=c495) (次小生成樹裸題，範測 2 有誤，應該是 37 38)
[Codeforeces 191C. Fools and Roads](https://codeforces.com/problemset/problem/191/C) (樹上「邊」差分，提示 : 改一點 `add()` 跟輸出的東西就好)
[Codeforeces 587C. Duff in the Army](https://codeforces.com/problemset/problem/587/C)
[AtCoder Beginner Contest 187E. Through Path](https://atcoder.jp/contests/abc187/tasks/abc187_e?lang=en) (前綴和)
[Codeforeces 519E. A and B and Lecture Rooms](https://codeforces.com/contest/519/problem/E) (動動腦)
[Codeforeces 178B3. Greedy Merchants](https://codeforces.com/contest/178/problem/B3)
[Codeforeces 466E. Information Graph](https://codeforces.com/contest/466/problem/E) (好像不是 LCA)

----

## 參考資料
- [海大競程 - Tree Algorithm 1](https://hackmd.io/@LeeShoWhaodian/2025Tree1#/)
- [Yui Huang 演算法學習筆記 - 【筆記】Lowest Common Ancestor 最近共同祖先](https://yuihuang.com/lowest-common-ancestor/)
- [Ian Shih - LCA (Lowest Common Ancestor / 最近共同祖先)](https://hackmd.io/@konchin/ryhgIlhMu)
- [台師大演算法筆記 - rooted tree](https://web.ntnu.edu.tw/~algo/RootedTree.html)
- [LCA problems](https://codeforces.com/blog/entry/43917)
- [OI Wiki - 前缀和 & 差分](https://oi-wiki.org/basic/prefix-sum/#%E6%A0%91%E4%B8%8A%E5%89%8D%E7%BC%80%E5%92%8C)
- [CP-Algorithms - Second Best Minimum Spanning Tree](https://cp-algorithms.com/graph/second_best_mst.html)

----

> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2025/8/17