LCA 学习笔记

概念

LCA

LCA (Lowest Common Ancestor),即最近公共祖先,指的是一棵树中任意两个节点深度最大的共同祖先。

有啥用

树有一个性质,两点之间有且只有一条简单路径,如果我们把 1 号节点作为根,则任意两点 \(x,y\) 的简单路径就是 \(x\)\(lca(x,y)\) 再到 \(y\)

所以这个东西大有用处,于是如何快速的求出两点之间的 \(LCA\) 就非常重要了。

朴素算法

我们通过一遍深搜处理出所有节点的父节点以及深度(这里的深度指到根节点的路径上的节点个数,与边权无关),然后对于每一次询问,我们先把更深的节点一步一步往上跳到与另一个节点一样的深度,然后再一起一步一步往上跳,直到跳到同一个节点,这个节点就是它们的 LCA。

这样每次询问的时间复杂度是树的深度,在精心构造的数据中可以达到 \(O(n)\),还是比较慢的。

倍增

思想

我们设 \(p_{i,j}\) 表示 \(i\) 号节点的第 \(2^j\) 级祖先的节点编号(1 级祖先即为父节点,0 级祖先为自己),若不存在,则 \(p_{i,j}=-1\)

我们考虑加速刚才朴素算法的过程。

  • 把两个节点跳到同一高度

对于两个节点 \(x,y\),设他们的深度分别为 \(d_x,d_y\)\(d_x \le d_y\),则我们需要把 \(y\) 变成 \(y\) 的第 \(d_x-d_y\) 级祖先,设 \(num=d_x-d_y\)。我们可以倍增地往上跳,从大到小枚举 2 的幂,若当前 2 的幂 \(2^j\) 满足 \(2^j\le num\),就把 \(y\) 更新为 \(p_{y,j}\),然后 \(num\) 减去 \(2^j\) 即可。这样的时间复杂度是 \(O(\log num)\) 的,若一棵树深度为 \(n\),则 \(O(\log num)\) 最坏为 \(O(\log n)\)

  • 一起往上跳

还是同样的思想,我们依然是从大到小去枚举 2 的幂,对于 \(2^j\) 来说,如果 \(p_{x,j} \not= p_{y,j}\),我们就把 \(x\) 变为 \(p_{x,j}\)\(y\) 变为 \(p_{y,j}\)。最后 \(x,y\) 并不是答案,而是 \(lca(x,y)\) 的两个子节点,所以 \(p_{x,0}\)(或 \(p_{y,0}\))才是答案。这样的时间复杂度也是 \(O(\log n)\) 的。

所以倍增对于每次询问的时间复杂度是 \(O(\log n)\) 的,很明显是快了很多。

实现

  • 初始化

我们先一遍深搜求出每个结点的层级,父节点,然后对于 \(p_{i,j}\),我们可以这样递推:\(p_{i,j}=p_{p_{i,j-1},j-1}\),时间复杂度为 \(O(n \log n)\)

void dfs(int x, int pr, int lv) {
	lvl[x] = lv, p[x][0] = pr;
	for (int i = 0; i < (int)e[x].size(); i++)
		if (e[x][i].to != pr)	
			dfs(e[x][i].to, x, lv + 1);
}
void initP() {
	for (int i = 0; i <= 20; i++)
		pw[i] = (1 << i);
	for (int j = 1; pw[j] <= n; j++)
		for (int i = 1; i <= n; i++)
			p[i][j] = p[p[i][j - 1]][j - 1];
}
  • 查询第 \(num\) 级祖先

代码很简短,与上面说的一样。

int Qry(int ch, int num) {
	int ans = ch;
	for (int j = 20; j >= 0; j--)
		if (pw[j] <= num)
			ans = p[ans][j], num -= pw[j];
	return ans;
}
  • 查询 \(lca\)

代码不长,思想前面已经说过。

int lca(int x, int y) {
	if (lvl[x] < lvl[y])
		swap(x, y);
	x = Qry(x, lvl[x] - lvl[y]);
	if (x == y)
		return x;
	for (int j = 20; j >= 0; j--)
		if (p[x][j] != p[y][j])
			x = p[x][j], y = p[y][j];
	return p[x][0];
}

一些例题

【模板】最近公共祖先(LCA)

题目链接:【模板】最近公共祖先(LCA)

思路:

模板,记得要加输入输出优化(scanf 和 printf)。

代码:

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 5e5 + 5;
const int MAX_LOG_N = 30;
struct Edge {
	int to, val;
	Edge(int _to, int _val) {
		to = _to, val = _val; 
	}
};
vector<Edge> e[MAXN];
int n, p[MAXN][MAX_LOG_N] = {{0}}, lvl[MAXN] = {0};
int pw[MAX_LOG_N] = {0};
void dfs(int x, int pr, int lv) {
	lvl[x] = lv, p[x][0] = pr;
	for (int i = 0; i < (int)e[x].size(); i++)
		if (e[x][i].to != pr)	
			dfs(e[x][i].to, x, lv + 1);
}
void initP() {
	for (int i = 0; i <= 20; i++)
		pw[i] = (1 << i);
	for (int j = 1; pw[j] <= n; j++)
		for (int i = 1; i <= n; i++)
			p[i][j] = p[p[i][j - 1]][j - 1];
}
int Qry(int ch, int num) {
	int ans = ch;
	for (int j = 20; j >= 0; j--)
		if (pw[j] <= num)
			ans = p[ans][j], num -= pw[j];
	return ans;
}
int lca(int x, int y) {
	if (lvl[x] < lvl[y])
		swap(x, y);
	x = Qry(x, lvl[x] - lvl[y]);
	if (x == y)
		return x;
	for (int j = 20; j >= 0; j--)
		if (p[x][j] != p[y][j])
			x = p[x][j], y = p[y][j];
	return p[x][0];
}
bool chk(int x, int y) {
	return (lvl[x] >= lvl[y] && Qry(x, lvl[x] - lvl[y]) == y);
}  
int m, s;
int main() {
	cin >> n >> m >> s;
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		e[u].push_back(Edge(v, 0));
		e[v].push_back(Edge(u, 0));
	}
	dfs(s, 0, 1), initP();
	for (int i = 1, u, v; i <= m; i++) {
		cin >> u >> v;
		cout << lca(u, v) << endl;;
	}
	return 0;
} 

posted @ 2022-12-28 09:34  rlc202204  阅读(51)  评论(0编辑  收藏  举报