1. 程式人生 > 其它 >LCA(最近公共祖先)總結

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 的時候, 我們需要做這些事:

  1. 遍歷他所有的兒子.
  2. 檢視有沒有與當前節點有關的問題 \((cur, x)\)
    , 如果有且 x 訪問過, 我們便可以從 x 節點開始向上尋找第一個 \(fa[i] = i\) 的節點, 這個節點便是這 (cur, x) 的最近公共祖先.
  3. 更新節點 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;
}

倍增法


與這個演算法有一點關係的東西(ST表)

慢一點, 但我覺得更好寫一些.

直接貼原始碼了, 不是很難, 註釋(雖然只有核心程式碼寫了一些)寫的應該比較清楚, 實在不行看洛谷題解吧, 我懶得寫了(

#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 中至少有兩個相同, 分類討論:

  1. 三個都相同, 則他們的 lca 即為聚會地點
  2. 有兩個相同, 令其為 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;
}

咕咕咕, 後面還會有例題的.