# 最低共同祖先 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