--- tags: 109_FDCS --- # LCA (Lowest Common Ancestor / 最近共同祖先) ## 何謂LCA 在一個有根樹(或DAG)上,同時擁有 $u, v$ 兩個點作為後代的點即為 $u, v$ 兩點的 $LCA$ i.e. ![](https://i.imgur.com/ePU8X5N.png =500x) 另外要注意,一個點本身也是自己的祖先 所以 $LCA(x,v)=x$ --- ## 如何求LCA 隨便都可以想到 $O(n)$ 的作法,只要DFS過去就好 其中一個可行的算法如下,假設我們要對 $u, v$ 做LCA 可以對其中一個點,先假設是 $u$ 一直往上找,直到 $u'$ 成為 $v$ 的祖先 i.e. ![](https://i.imgur.com/11LTM0v.png =450x) 黃色框代表是 $v$ 的祖先,紅色則是往上找的的路徑 ```cpp= int LCA(int u, int v) { if(isanc(u, v)) return u; if(isanc(v, u)) return v; while(!isanc(u, v)) u = anc[u]; return u; } ``` 其中`isanc`是用來判斷第一個引數是否為第二個引數的祖先 `anc`則是用來儲存每個點的父節點 `anc`很直覺,DFS就有了 `isanc`則需要借助另外兩個陣列`tin`和`tout`來實現 i.e. ```cpp= bool isanc(int a, int b) { return tin[a] <= tin[b] && tout[a] >= tout[b]; } ``` `tin`和`tout`分別表示DFS進入及離開的時間點 ![](https://i.imgur.com/TjudB5C.png =450x) 從這張圖可以發現,一個子樹的根節點`tin`的值都會小於子節點`tin`的值 根節點`tout`的值都會大於子節點`tout`的值 所以只要有每個點`tin`和`tout`的值就可以判斷兩個點是否為對方的祖先 預處理具體實現如下 ```cpp= void build(int cur, int pre) { static int t = 0; anc[cur] = pre; tin[cur] = t++; for(const auto& i : edge[cur]) if(i.b != pre) build(i.b, cur); tout[cur] = t++; } ``` 由於一次往上找一格,所以這個演算法的時間複雜度為 $O(n)$ 但很可惜有 <ruby>$O(logn)$<rt>更快的</rt></ruby> 作法,所以題目不會簡單的放過你 所以接下來就要介紹倍增法 --- ## 倍增法 其實不難看出來,上面那種 $O(n)$ 的作法已經很接近 $O(logn)$ 的作法了 與上面方法不同的點在於,往上找祖先時,一次多往上找幾個 i.e. ![](https://i.imgur.com/qh6hDpy.png =450x) 乍看之下紅線好像毫無邏輯,但數一下可以發現 這些線都是往上 $2$ 的倍數父節點 觀察 $u$ 的祖先 i.e. ![](https://i.imgur.com/vk4QvPV.png =150x) 可以發現節點 $2$ 以上的點都是 $v$ 的祖先 節點 $5$ 以下都不是,很明顯,這符合進行二分搜所需的單調性 有很多方法可以實作這個二分搜,這裡使用實作起來相對簡單的方法 也就是找到不是 $v$ 的祖先的最高的節點,之後再回傳父節點就是LCA 已知根節點一定是 $u, v$ 的祖先,所以最差情況需要往上走 $n-1$ 步 這代表我們可以用小於 $n$ 以內的 $2$ 的冪次來組成 $n$ 以內的任何步數 以上面的圖來舉例,先往上找 $4$ 倍祖先,也就是節點 $1$ 節點 $1$ 已經是 $v$ 的祖先,所以不更新 $u$ 接下來往上找 $2$ 倍祖先的節點 $5$ 節點 $5$ 不是 $v$ 的祖先,所以將 $u$ 更新成節點 $5$的位置 再來往上找 $1$ 倍祖先,節點 $2$ 節點 $2$ 已經是 $v$ 的祖先,所以不更新 $u$ 最後回傳 $u$ 所在位置 $5$ 的父節點,也就是 $2$ 節點 $2$ 即是 $u, v$ 的LCA ### 預處理 對於每個節點,求出 $2^0,2^1,2^2...2^{logn}$ 倍的祖先 可以透過一個DFS來完成 一開始先將所有節點的所有倍祖先都先設成根節點 ```cpp=1 V<V<int>> anc(n, V<int>(__lg(n))); V<int> tin(n), tout(n); ``` 之後做DFS,引數為當前節點和當前節點的父節點,並記錄`tin`和`tout`的時間點 ```cpp=3 F<void(int, int)> build = [&](int cur, int pre) { static int t = 0; tin[cur] = t++; //some operations for (const auto& i : v[cur]) if (i != pre) build(i, cur); tout[cur] = t++; }; ``` 在每個節點我們可以透過已經更新過的節點來更新當前節點的祖先 i.e. 當前節點的 $2^1$ 倍祖先就是 $2^0$ 倍祖先的 $2^0$ 倍祖先 而當DFS到當前節點時,代表祖先都已經DFS過了 code如下 ```cpp=6 int k = pre; for (int i = 0; i <= __lg(n); i++) anc[cur][i] = k, k = anc[k][i]; ``` 將上面的code合起來,`build`函式如下 ```cpp= F<void(int, int)> build = [&](int cur, int pre) { static int t = 0; tin[cur] = t++; int k = pre; for (int i = 0; i <= __lg(n); i++) anc[cur][i] = k, k = anc[k][i]; for (const auto& i : v[cur]) if (i != pre) build(i, cur); tout[cur] = t++; }; ``` ### 查詢是否為祖先 利用`tin`和`tout` 上面提過,不贅述 ```cpp= auto isanc = [&](int a, int b) { return tin[a] <= tin[b] && tout[a] >= tout[b]; }; ``` ### 查詢LCA 如果其中一個點為另一個點的祖先,則回傳 ```cpp= if (isanc(a, b)) return a; if (isanc(b, a)) return b; ``` 否則,利用二分搜往上找 原理上面也提過,多看兩次就會了 ```cpp=3 for (int i = __lg(n); i >= 0; i--) if (!isanc(anc[a][i], b)) a = anc[a][i]; ``` 最後,因為上面所求結果會是LCA的子節點 回傳時回傳父節點即為LCA ```cpp=6 return anc[a][0]; ``` 綜合起來就是查詢LCA的函式 ```cpp= auto lca = [&](int a, int b) { if (isanc(a, b)) return a; if (isanc(b, a)) return b; for(int i = __lg(n); i >= 0; i--) if (!isanc(anc[a][i], b)) a = anc[a][i]; return anc[a][0]; }; ``` --- ## 例題: [ZJ d767](https://zerojudge.tw/ShowProblem?problemid=d767) ### 題敘 給定一顆樹,根為節點 $1$ 對每筆詢問求兩節點之 $LCA$ 及兩節點之間的距離 ### 策略 $LCA$ 用倍增法就可求得 兩點間的距離可以多維護一個陣列 $d$ ,代表到根節點的距離 因為在樹上兩點之間只有一條路徑 代表該條路徑一定會通過 $LCA$ 所以兩點間距離就是 $distance(u,LCA(u,v))+distance(v,LCA(u,v))$ 而 $distance(u, LCA(u,v)) = d_u-d_{LCA(u,v)}$ $distance(v,LCA(u,v))$ 同理 最終得出 $distance(u,v)=d_u+d_v-2\times d_{LCA(u,v)}$ ### code ```cpp= template<typename T> using V = vector<T>; template<typename T> using F = function<T>; inline void solve() { int n, q; cin >> n >> q; V<V<int>> v(n), anc(n, V<int>(__lg(n) + 1)); V<int> in(n), out(n), d(n); for (auto& i : v) { static int ch; while (cin >> ch, ch) i.emplace_back(ch - 1); } F<void(int, int, int)> build = [&](int cur, int pre, int depth) { static int t = 0; in[cur] = t++; d[cur] = depth; for (int i = 0, k = pre; i <= __lg(n); i++) anc[cur][i] = k, k = anc[k][i]; for (auto& i : v[cur]) if (i != pre) build(i, cur, depth + 1); out[cur] = t++; }; auto isanc = [&](int a, int b) { return in[a] <= in[b] && out[a] >= out[b]; }; auto query = [&](int a, int b) { if (isanc(a, b)) return a; if (isanc(b, a)) return b; for (int i = __lg(n); i >= 0; i--) if (!isanc(anc[a][i], b)) a = anc[a][i]; return anc[a][0]; }; build(0, 0, 0); while (q--) { static int a, b, tmp; cin >> a >> b, a--, b--; tmp = query(a, b); cout << tmp + 1 << ' ' << d[a] + d[b] - 2 * d[tmp] << endl; } } ```