求 LCA 的三種方法


(YYL: LCA 有三種求法, 你們都知道么?)

(眾神犇: 這哪里來的傻叉...)

1. 樹上倍增

對於求 LCA, 最朴素的方法是"讓兩個點一起往上爬, 直到相遇", "如果一開始不在同一深度, 先爬到同一深度". 樹上倍增求 LCA 的方法同樣基於這個道理, 只不過利用了倍增思想從而加速了"向上爬"的操作. 也就是說, 每次向上爬的高度不是 1, 而是 2 的冪.

我們用 $f(i, j)$ 表示從節點 $i$ 向上爬 $2^j$ 的高度所到達的節點, 則 $f(i, 0)$ 就代表節點 $i$ 的父節點. 那么對於任意的 $f(i, j), j > 0$, 有

$f(i, j) = f(f(i, j-1), j-1)$.

當我們要求兩點的 LCA 時, 先讓它們到同一高度. 這個過程我們使用二進制拆分來加速. 比如當兩點高度相差 $5$ 時, $(5)_{10} = (101)_2$, 那么我們就讓高度較小的那個節點先往上爬 $2^2 = 4$ 步, 再往上 $2^0 = 1$ 步. 此時兩點即在同一高度.

如果爬到同一高度后兩點相同, 顯然這個點就是它們的 LCA, 直接返回即可.

如果兩點不同, 就一起往上爬. 這是一個無限逼近的過程, 直到找到它們的 LCA 的子節點為止. 詳見代碼.

 1 for (int i = 1; i <= n; ++i)
 2     lg[i] = lg[i - 1] + (1 << lg[i - 1] + 1 == i);
 3 
 4 int lca(int x, int y) {
 5     if (dep[x] < dep[y])
 6         swap(x, y);
 7     while (dep[x] > dep[y])
 8     x = f[x][lg[dep[x] - dep[y]]];
 9     if (x == y)
10         return x;
11     for (int k = lg[dep[x]]; k >= 0; --k)
12         if (f[x][k] != f[y][k])
13             x = f[x][k], y = f[y][k];
14     return f[x][0];
15 }

(上面的代碼預先算出了 $log_2 (n)$ 的值, 從而簡化了代碼.)

 

2. Tarjan 算法

Tarjan 算法建立在 DFS 的基礎上. 

假如我們正在遍歷節點 x, 那么根據所有節點各自與 x 的 LCA 是誰, 我們可以將節點進行分類: x 與 x 的兄弟節點的 LCA 是 x 的父親, x 與 x 的父親的兄弟節點的 LCA 是 x 的父親的父親, x 與 x 的父親的父親的兄弟節點的 LCA 是 x 的父親的父親的父親... 將這些類別各自歸入不同的集合中, 如果我們能夠維護好這些集合, 就能夠很輕松地處理有關 x 節點的 LCA 的詢問. 顯然我們可以使用並查集來維護.

Tarjan 算法的大致步驟如下:

1. 遍歷 x 節點的子節點. 對於 x 節點的每個子節點, 該子節點遍歷結束之后, 將其整棵子樹合並到 x, 並保證合並之后祖先為 x;

2. 將 x 標記為已遍歷;

3. 處理有關 x 的詢問. 對於詢問 (x, y), 如果 y 節點已遍歷, 則 x 與 y 的 LCA 就是 y 節點所在集合的祖先; 否則, 將其推遲到遍歷 y 時再處理.

代碼如下:

 1 void tarjan(int u) {
 2     fa[u] = u;
 3     
 4     int i, v;
 5     for (i = 0; i < tree[u].size(); i++) {
 6         v = tree[u][i];
 7         tarjan(v);
 8         fa[findset(v)] = u;
 9     }
10     
11     vis[u] = true;
12     
13     for (i = 0; i < query[u].size(); i++) {
14         if (vis[query[u][i]]) {
15             cnt[findset(query[u][i])]++;
16         }
17     }
18 }

(對於保證合並之后集合祖先為 x 這一步驟, 網絡上的代碼大多使用了一個 ancestor 數組來記錄集合的祖先是誰. 原因是如果使用並查集的帶秩合並, 合並兩個集合之后不好確定根節點到底是誰. 但是帶秩合並在有路徑壓縮的情況下作用有限, 所以這里取消了帶秩合並而直接使用 fa[findset(v)] = u 來保證集合的祖先為 u.)

 

3. LCA 轉 RMQ

樹上的一些問題可以轉化為對樹的 DFS 序列的操作. 比如對於這樣一棵樹:

(圖片來自 http://scturtle.is-programmer.com/posts/30055.html)

對於以 3 這個節點為根的整棵子樹, 其 DFS 序列為: 3 7 3 8 9 11 9 8 10 12 10 8 3.

假如我們要詢問 7 和 12 的 LCA, 我們找到 7 和 12 分別第一次出現的位置, 然后在這一個區間內找到深度最小的那個節點, 也就是節點 3, 顯然它就是 7 和 12 的 LCA.

記 DFS 序列為 $S[1...2n]$, 節點 $x$ 在序列 $S$ 中第一次出現的位置為 $E[x]$, 用 $RMQ(L, R)$ 表示序列 $S$ 中深度最小的那個節點. 則

$LCA(u, v) = RMQ(E[u], E[v])$

代碼略. DFS + RMQ 的普通做法即可(ST, 線段樹等等).


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM