最近公共祖先(LCA)笔记
一、LCA 是什么
最近公共祖先是指对于有根树T的两个结点 $u$、$v$,满足一个结点 $x$ 既是 $u$ 的祖先,也是 $v$ 的祖先,且 $x$ 的深度尽可能大。在这里,一个节点也可以是它自己的祖先。
如下图所示,当$u=4$,$v=7$时:
$u$ 的祖先有:$4$、$\mathbf{2}$、$\mathbf{1}$。
$v$ 的祖先有:$7$、$5$、$\mathbf{2}$、$\mathbf{1}$。
它们的公共祖先分别有 $2$、$1$,其中 $2$ 的深度最大,因此, $2$ 是 $4$ 和 $7$ 的最近公共祖先 (虽然说肉眼也能看出来吧) 。

二、LCA 如何实现
1. 枚举法:
把 $u$ 的所有祖先标记起来,再把 $v$ 的所有祖先标记起来。此方法最为简单,也最为无脑,查询一次的时间复杂度高达 $O(n)$,大部分题目都无法通过。
2. 倍增法
(1). 思路
先把要查询的 $u$ 与 $v$ 拉到同一层,然后按照 $2^i$、$2^{i-1}$、$2^{i-2}$ ... $2^1$、$2^0$ 的顺序枚举步数,使其跳跃到达的深度越大越好,最后到达其最近公共祖先的下一层结束,返回结果。
(2). 做法
①. 建图
我们需要采用一种容器来存储这张树,在这里选择 链式前向星 的原因,是其优秀的 时间复杂度 与 空间复杂度,能够容纳下大量的数据
struct EDGE{
int v,next;//v表示编号为i的边的终点,next表示以第i条边的起点为起点的下一条边的编号。
}edge[N];
int head[N],idx=1;//head[i]表示以i为起点的最后一条加入的边的编号。
void add_edge(int u,int v){
edge[idx].v=v;//将终点设置为终点。
edge[idx].next=head[u];//第idx条边的下一条边的编号即为以u为起点的加入的最后一条边的编号。
head[u]=idx;//以u为起点的最后一条加入的边更新。
idx++;//编号跳到下一条边。
}
②. 确定深度与每步跳到的点位
确定了容器之后,我们就要确定每一个点的 深度 与它跳 $2^i$ 步能到达的点的 编号。
首先,我们 确定一个根节点 作为整棵树的根,然后用深搜算法遍历这颗树的每一个节点,一路一边将他们的层数设置为他们的祖先的上一层,一边用状态转移方程将他们跳 $2^i$ 步能到达的点传下去。
若用 $dep_i$ 表示编号为 $i$ 的节点的深度,用 $u$ 来表示当前节点的编号,用 $fa$ 来表示其父亲节点的编号,那么就可以很容易的得到:$$dep_u=dep_{fa}+1$$
若用 $f_{i,j}$ 来表示编号为 $i$ 的节点跳 $2^j$ 步能到达的节点的编号,那么就相当于从 $i$ 出发先跳 $2^{j-1}$ 步再跳 $2^{j-1}$ 步,所以状态转移方程即为:$$f_{i,j}=f_{f_{i,j-1},j-1}$$
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=edge[i].pre){//从以u为起点的加入的最后一条边开始遍历,遍历所有以u为起点的边。
int j=edge[i].v;//取出当前边的终点编号。
if(j==fa)continue;//如果当前这条边的终点是以u为终点的边的起点,那么说明构成了一个环,不执行。
f[j][0]=u;//跳2^0(1)步到达的点即为u。
for(int k=1;(1<<k)<=dep[u];k++){
f[j][k]=f[f[j][k-1]][k-1];//状态转移
}
dfs(j,u);//进入以j为起点的下一组边(下一层
}
}
③. 倍增找公共祖先
设要寻找公共祖先的两个点的编号分别为 $x$,$y$。那么可以分为四步:
第一步:将 $x$ 换为深度较大的一个,方便操作。
第二步:按照 $2$ 的次幂的步数枚举,将 $x$ 跳至与 $y$ 同等的深度。
第三步:让 $x$ 与 $y$ 共同前进,但是不能让它们跳到它们的公共祖先上。
第四步:此时,$x$ 与 $y$ 都会恰恰位于它们最近公共祖先的下一层,因此只需要让它们再跳 $1$ 步即可。
代码如下所示
int lca(int x,int y){
//第一步
if(dep[x]<dep[y])swap(x,y);
//第二步
for(int i=19;i>=0;i--){
if(dep[f[x][i]]>=dep[y]){
x=f[x][i];
}
}
//若此时已经找到祖先了,就return。
if(x==y)return y;
//第三步
for(int i=19;i>=0;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
//第四步
return f[y][0];
}
3. Tarjan 法
我不会QAQ
为什么用 Tarjan,倍增怎么你了
想要极致的时间复杂度还得人造 vector
趁朱老师发现之前,开溜
三、例题
1.【模板】最近公共祖先(LCA)
| 平台 | 网址 |
|---|---|
| 洛谷 | https://www.luogu.com.cn/problem/P3379 |
| 南海信息学 | https://nh.51goc.com/studentCourse/class?groupId=301&courseId=198&contestId=9993#testTab=9994_2 第一题 |
模板题,直接上代码:
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct EDGE{
int v,next;
}edge[N*2];
int n,m,root,head[N],dep[N],f[N][20],id=1;
void un(int u,int v){
edge[id].v=v;
edge[id].next=head[u];
head[u]=id;
id++;
}
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=edge[i].next){
int j=edge[i].v;
if(j==fa)continue;
f[j][0]=u;
for(int k=1;(1<<k)<=dep[u];k++){
f[j][k]=f[f[j][k-1]][k-1];
}
dfs(j,u);
}
}
int lca(int x,int y){
if(dep[x]<dep[y])swap(x,y);
for(int i=19;i>=0;i--){
if(dep[f[x][i]]>=dep[y]){
x=f[x][i];
}
}
if(x==y)return y;
for(int i=19;i>=0;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[y][0];
}
int main(){
scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
un(u,v);
un(v,u);
}
dfs(root,0);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}

浙公网安备 33010602011771号