LCA 学习笔记

概念

LCA

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

有啥用

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

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

朴素算法

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

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

倍增

思想

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

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

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

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

  • 一起往上跳

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

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

实现

  • 初始化

我们先一遍深搜求出每个结点的层级,父节点,然后对于 pi,j,我们可以这样递推:pi,j=ppi,j1,j1,时间复杂度为 O(nlogn)

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 @   rlc202204  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示