最近公共祖先(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;
}
}

浙公网安备 33010602011771号