LCA(最近公共祖先)總結
先留個坑
我回來了(
什麼是 LCA
對於有根樹T的兩個結點u、v,最近公共祖先LCA(u,v)表示一個結點x,滿足x是u和v的祖先且x的深度儘可能大。在這裡,一個節點也可以是它自己的祖先。
以上, 百度百科中的定義, 不僅是你, 我也看不懂.
簡單點說, 就是一棵有根樹中兩個節點的最近公共祖先, 下面我還是畫圖舉例說明.
這次用的是這個畫的樹, 感覺有點奇怪不過沒辦法了(
若上圖中樹根為1, 則有:
\(lca(3, 7)=1\)
\(lca(3, 2)=2\)
\(lca(3, 4)=2\)
等等
程式問題
tarjan 法
好處是速度更快, 時間複雜度\(O(n + m)\)
可以根據這篇blog中的圖分析問題, 強烈建議帶上紙筆跟著文字畫一畫.
思路講一下:
首先儲存每一個節點的父親與兒子, 讀取問題之後用 dfs 遍歷的同時得出答案, 是一個離線演算法.
P.S. 用到了並查集的思想.
我們初始化每個節點的父親(fa[])為它自身, 當節點訪問完後再更新他真正的父親(樹根的父親是他自己). 對於每一個訪問過的節點 \(i\), 如果 \(fa[i] = i\), 那麼這個節點就沒有訪問完, 就一定是當前正在訪問中的節點的祖先.
而在 dfs 搜尋節點 cur 的時候, 我們需要做這些事:
- 遍歷他所有的兒子.
- 檢視有沒有與當前節點有關的問題 \((cur, x)\)
- 更新節點 cur 的狀態.
具體的還是看程式碼比較好吧(
#include <stdio.h> #include <iostream> #include <vector> using namespace std; int n, m, s; int fa[500003]; int head[500003], next[1000003], val[1000003], cnt = 0; struct NODE { int x; int id; }; vector <NODE> q[500003]; int ans[500003]; bool vis[500003]; // 其實這東西似乎可以不要, 不過用了之後似乎會便於理解? inline void add (int x, int y) { val[++cnt] = y; next[cnt] = head[x]; head[x] = cnt; return; } inline void init () { for (int i = 1; i <= n; i++) fa[i] = i; return; } inline int find (int x) { // 向上尋找 if (fa[x] != x) return find(fa[x]); return x; } inline void tarjan (int cur, int father) { // dfs // 1. 訪問所有兒子 for (int i = head[cur]; i; i = next[i]) { int x = val[i]; if (x == father) continue; tarjan(x, cur); } // 2. 檢視所有與當前節點有關係的問題並回答能回答的 for (int i = 0; i < q[cur].size(); i++) { int x = q[cur][i].x; if (vis[x]) ans[q[cur][i].id] = find(x); } // 3. 更新當前節點的資訊 vis[cur] = true; fa[cur] = cur == s ? s : father; // 注意 s (樹根) 的父親是他自己 return; } int main() { scanf("%d %d %d", &n, &m, &s); for (int i = 1; i < n; i++) { int a, b; scanf("%d %d", &a, &b); add(a, b); add(b, a); } for (int i = 1; i <= m; i++) { NODE nd; nd.id = i; int x, y; scanf("%d %d", &x, &y); nd.x = x, q[y].push_back(nd); nd.x = y, q[x].push_back(nd); } init(); tarjan(s, 0); for (int i = 1; i <= n; i++) printf("%d\n", ans[i]); return 0; }
倍增法
慢一點, 但我覺得更好寫一些.
直接貼原始碼了, 不是很難, 註釋(雖然只有核心程式碼寫了一些)寫的應該比較清楚, 實在不行看洛谷題解吧, 我懶得寫了(
#include <stdio.h>
#include <iostream>
using namespace std;
int n, m, s;
int head[500003], val[1000003], last[1000003], cnt = 0;
int fa[500003][50], depth[500003];
int lg[500003];
inline void add(int x, int y) {
val[++cnt] = y;
last[cnt] = head[x];
head[x] = cnt;
return;
}
inline int lca(int x, int y) {
// 1. 使得較深的那個節點向上移, 直至兩個節點高度相同
if (depth[x] < depth[y])
swap(x, y);
while (depth[x] > depth[y])
x = fa[x][lg[depth[x] - depth[y]] - 1];
// 2. 兩個節點一起向上移, 直至兩個節點重合
if (x == y) // 特判, 即 y 是 x 的祖先的情況
return x;
for (int k = lg[depth[y]] - 1; k >= 0; k--)
if (fa[x][k] != fa[y][k])
x = fa[x][k], y = fa[y][k];
return fa[x][0];
}
void dfs(int cur, int father, int high) { // 獲取所有節點的祖先和深度
depth[cur] = high;
fa[cur][0] = father;
for (int i = 1; i <= lg[depth[cur]]; i++)
fa[cur][i] = fa[fa[cur][i-1]][i-1];
for (int i = head[cur]; i; i = last[i])
if (val[i] != father)
dfs(val[i], cur, high + 1);
return;
}
inline void init() {
for (int i = 1; i <= n; i++)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
return;
}
int main() {
scanf("%d %d %d", &n, &m, &s);
init();
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d %d", &x, &y);
add(x, y); add(y, x);
}
dfs(s, 0, 1);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
printf("%d\n", lca(x, y));
}
return 0;
}
咕咕咕 應該還有後續內容(例題), 等著吧(
例題 [BZOJ1832 [AHOI2008]聚會]
題目描述
Y島風景美麗宜人,氣候溫和,物產豐富。
Y島上有N個城市(編號1,2,…,N),有N-1條城市間的道路連線著它們。
每一條道路都連線某兩個城市。
幸運的是,小可可通過這些道路可以走遍Y島的所有城市。
神奇的是,乘車經過每條道路所需要的費用都是一樣的。
小可可,小卡卡和小YY經常想聚會,每次聚會,他們都會選擇一個城市,使得3個人到達這個城市的總費用最小。
由於他們計劃中還會有很多次聚會,每次都選擇一個地點是很煩人的事情,所以他們決定把這件事情交給你來完成。
他們會提供給你地圖以及若干次聚會前他們所處的位置,希望你為他們的每一次聚會選擇一個合適的地點。
輸入格式
第一行兩個正整數,N和M,分別表示城市個數和聚會次數。
後面有N-1行,每行用兩個正整數A和B表示編號為A和編號為B的城市之間有一條路。
再後面有M行,每行用三個正整數表示一次聚會的情況:小可可所在的城市編號,小卡卡所在的城市編號以及小YY所在的城市編號。
輸出格式
一共有M行,每行兩個數Pos和Cost,用一個空格隔開,表示第i次聚會的地點選擇在編號為Pos的城市,總共的費用是經過Cost條道路所花費的費用。
輸入樣例#1
6 4
1 2
2 3
2 4
4 5
5 6
4 5 6
6 3 1
2 4 4
6 6 6
輸出樣例#1
5 2
2 5
4 1
6 0
資料範圍
N≤500000,M≤500000
可以畫圖, 發現三個節點的 lca 中至少有兩個相同, 分類討論:
- 三個都相同, 則他們的 lca 即為聚會地點
- 有兩個相同, 令其為 x, 另一個為 y, 則 x 一定是最佳的聚會地點, 具體的話可以畫圖, 用一個類似於貪心的思想去證明
所以我們只需要求出三個節點中兩兩的 lca, 然後幾個 if 判斷即可, 距離的話加一個字首和(深度)就可以了.
程式碼如下
#include <stdio.h>
#include <iostream>
using namespace std;
int n, m;
int head[500003], val[1000003], last[1000003], cnt = 0;
int fa[500003][50], depth[500003];
int lg[500003];
inline void add(int x, int y) {
val[++cnt] = y;
last[cnt] = head[x];
head[x] = cnt;
return;
}
inline int lca(int x, int y) {
if (depth[x] < depth[y])
swap(x, y);
while (depth[x] > depth[y])
x = fa[x][lg[depth[x] - depth[y]] - 1];
if (x == y)
return x;
for (int k = lg[depth[y]] - 1; k >= 0; k--)
if (fa[x][k] != fa[y][k])
x = fa[x][k], y = fa[y][k];
return fa[x][0];
}
void dfs(int cur, int father, int high) {
depth[cur] = high;
fa[cur][0] = father;
for (int i = 1; i <= lg[depth[cur]]; i++)
fa[cur][i] = fa[fa[cur][i-1]][i-1];
for (int i = head[cur]; i; i = last[i])
if (val[i] != father)
dfs(val[i], cur, high + 1);
return;
}
inline void init() {
for (int i = 1; i <= n; i++)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
return;
}
int main() {
scanf("%d %d", &n, &m);
init();
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d %d", &x, &y);
add(x, y); add(y, x);
}
dfs(1, 0, 1);
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
int lca1 = lca(x, y), lca2 = lca(y, z), lca3 = lca(z, x), ans;
if (lca1 == lca2)
ans = lca3;
else if(lca1 == lca3)
ans = lca2;
else
ans = lca1;
printf("%d %d\n", ans, depth[x] + depth[y] + depth[z] - depth[lca1] - depth[lca2] - depth[lca3]);
}
return 0;
}
咕咕咕, 後面還會有例題的.