古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

最近公共祖先(LCA ——Lowest Common Ancestor)的倍增算法

定义

两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

朴素算法

首先从深度大的点开始往根节点遍历,依次标记经过的点,然后深度小的节点开始遍历,找到第一个被标记的节点,那么这个节点就是最近的公共祖先。
朴素算法每次查询的复杂度是(n),当查询多次时会到达(nm),所以我们采用LCA倍增算法优化。

LCA倍增算法

本质上来说,倍增算法同样是向上去尝试找点,但是他尝试的方法不一样,它采用向上跳跃pow(2,k)的方法,他们的最近公共祖先肯定是一个点,那么可以先让深度大的点跳到跟深度小的点相同,再让他们一起往上跳,直到找到最近公共祖先为止.
所以我们得先处理一下树,找到每一个节点的深度,并且预处理往上跳pow(2,i)步后到达哪一个点,代码如下:

void dfs(i64 x,i64 faa) {
	fa [ x ] [ 0 ] = faa;//更新x往上跳pow(2,0)后到达faa节点
	dep [ x ] = dep [faa ] + 1;//子节点深度等于父节点加1
	for (int i = 1; i < 31; ++i) {
		fa[x][i] = fa[fa[x][i - 1]][i - 1];//往上跳pow(2,i)等于他往上跳pow(2,i-1)后再跳pow(2,i-1)
	}
	for (auto it : t[x]) {
		if (it != faa)//防止返回访问父节点
			dfs(it , x);
	}

}

预处理完成后,接下来我们得实现查询两个点的公共祖先,我们首先让他们跳到同一个高度,然后一起往上跳,直到到达为止;
这里有一个细节,就是跳跃高度尽可能从大的开始跳,这样子可以保证效率够高,并且我们知道,任意一个数都可以用二进制表示,所以我们一定可以通过预处理的点,实现跳到两个点的任意祖先节点。
代码如下:

i64 lca(i64 x , i64 y) {
	if (dep [ x ] > dep [ y ])//找到深度大的节点
		swap(x , y);
	i64 temp = dep [ y ] - dep [ x ];
	for (int j = 0; temp; j++ , temp >>= 1) {
		if (temp & 1)
		 y = fa [ y ] [ j ];//让y往上跳temp步,跟x深度相同
	}
	if (y == x)
		return x;//如果此时相等直接找到x是y的祖先
	//不然继续找
	for (int j = 30; j >= 0 && y != x; j--) {
		if (fa [ x ] [ j ] != fa [ y ] [ j ]) {
			x = fa [ x ] [ j ];//更新 x
			y = fa [ y ] [ j ];//更新 y
		}
	}
	return fa[y][0];//返回找到的公共祖先
}

于是我们就实现了基本的LCA倍增算法;
来写一道模板题

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

题目描述

如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

输入格式

第一行包含三个正整数 \(N,M,S\),分别表示树的结点个数、询问的个数和树根结点的序号。

接下来 \(N-1\) 行每行包含两个正整数 \(x, y\),表示 \(x\) 结点和 \(y\) 结点之间有一条直接连接的边(数据保证可以构成树)。

接下来 \(M\) 行每行包含两个正整数 \(a, b\),表示询问 \(a\) 结点和 \(b\) 结点的最近公共祖先。

输出格式

输出包含 \(M\) 行,每行包含一个正整数,依次为每一个询问的结果。

输入输出样例 #1

输入 #1

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

输出 #1

4
4
1
4
4

说明/提示

对于 \(30\%\) 的数据,\(N\leq 10\)\(M\leq 10\)

对于 \(70\%\) 的数据,\(N\leq 10000\)\(M\leq 10000\)

对于 \(100\%\) 的数据,\(1 \leq N,M\leq 5\times10^5\)\(1 \leq x, y,a ,b \leq N\)不保证 \(a \neq b\)

样例说明:

该树结构如下:

第一次询问:\(2, 4\) 的最近公共祖先,故为 \(4\)

第二次询问:\(3, 2\) 的最近公共祖先,故为 \(4\)

第三次询问:\(3, 5\) 的最近公共祖先,故为 \(1\)

第四次询问:\(1, 2\) 的最近公共祖先,故为 \(4\)

第五次询问:\(4, 5\) 的最近公共祖先,故为 \(4\)

故输出依次为 \(4, 4, 1, 4, 4\)

题目链接https://www.luogu.com.cn/problem/P3379

AC代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using i64 = long long;
i64 n , m,s;
vector <i64> t [ 500005 ];
bool b [ 500005 ];
i64 dep [ 500005 ];
i64 fa [ 500005 ] [ 31 ];
i64 ed , ans = 0;

void dfs(i64 x,i64 faa) {
	fa [ x ] [ 0 ] = faa;//更新x往上跳pow(2,0)后到达faa节点
	dep [ x ] = dep [faa ] + 1;//子节点深度等于父节点加1
	for (int i = 1; i < 31; ++i) {
		fa[x][i] = fa[fa[x][i - 1]][i - 1];//往上跳pow(2,i)等于他往上跳pow(2,i-1)后再跳pow(2,i-1)
	}
	for (auto it : t[x]) {
		if (it != faa)//防止返回访问父节点
			dfs(it , x);
	}

}
i64 lca(i64 x , i64 y) {
	if (dep [ x ] > dep [ y ])//找到深度大的节点
		swap(x , y);
	i64 temp = dep [ y ] - dep [ x ];
	for (int j = 0; temp; j++ , temp >>= 1) {
		if (temp & 1)
		 y = fa [ y ] [ j ];//让y往上跳temp步,跟x深度相同
	}
	if (y == x)
		return x;//如果此时相等直接找到x是y的祖先
	//不然继续找
	for (int j = 30; j >= 0 && y != x; j--) {
		if (fa [ x ] [ j ] != fa [ y ] [ j ]) {
			x = fa [ x ] [ j ];//更新 x
			y = fa [ y ] [ j ];//更新 y
		}
	}
	return fa[y][0];//返回找到的公共祖先
}
int main() {
	dep [ 0 ] = -1;
	cin >> n >> m>>s;
	for (int i = 1; i < n; i++) {
		i64 x , y;
		cin >> x >> y;
		t [ x ].push_back(y);
		t [ y ].push_back(x);
	}
	dfs(s , 0);
	for (int i = 1; i <= m; i++) {
		i64 x , y;
		cin >> x >> y;
		cout << lca(x , y) << endl;
	}

}
posted @ 2025-08-07 15:52  Morphis‘  阅读(66)  评论(0)    收藏  举报