Skip to content

一、LCA 是什么

最近公共祖先是指对于有根树T的两个结点 uv,满足一个结点 x 既是 u 的祖先,也是 v 的祖先,且 x 的深度尽可能大。在这里,一个节点也可以是它自己的祖先。

如下图所示,当u=4v=7时:

u 的祖先有:421

v 的祖先有:7521

它们的公共祖先分别有 21,其中 2 的深度最大,因此, 247 的最近公共祖先 (虽然说肉眼也能看出来吧)

二、LCA 如何实现

1. 枚举法:

u 的所有祖先标记起来,再把 v 的所有祖先标记起来。此方法最为简单,也最为无脑,查询一次的时间复杂度高达 O(n),大部分题目都无法通过。

2. 倍增法

(1). 思路

先把要查询的 uv 拉到同一层,然后按照 2i2i12i2 ... 2120 的顺序枚举步数,如果跳跃后不在同一个节点上,就跳跃,最后到达其最近公共祖先的下一层结束,返回结果。

(2). 做法

①. 建图

我们需要采用一种容器来存储这张树,在这里选择 链式前向星 的原因,是其优秀的 时间复杂度空间复杂度,能够容纳下大量的数据

cpp
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++;//编号跳到下一条边。
}

②. 确定深度与每步跳到的点位

确定了容器之后,我们就要确定每一个点的 深度 与它跳 2i 步能到达的点的 编号

首先,我们 确定一个根节点 作为整棵树的根,然后用深搜算法遍历这颗树的每一个节点,一路一边将他们的层数设置为他们的祖先的上一层,一边用状态转移方程将他们跳 2i 步能到达的点传下去。

若用 depi 表示编号为 i 的节点的深度,用 u 来表示当前节点的编号,用 fa 来表示其父亲节点的编号,那么就可以很容易的得到:

depu=depfa+1

若用 fi,j 来表示编号为 i 的节点跳 2j 步能到达的节点的编号,那么就相当于从 i 出发先跳 2j1 步再跳 2j1 步,所以状态转移方程即为:

fi,j=ffi,j1,j1
cpp
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;//如果当前这条边的终点是上一条边的起点,那么说明这其实是一条无向边添加进edge数组的两条边,不执行。
		f[j][0]=u;//跳2^0步到达的点即为u。
		for(int k=1;(1<<k)<=dep[u];k++){
			f[j][k]=f[f[j][k-1]][k-1];//状态转移
		}
		dfs(j,u);//继续搜索下一层
	}
}

③. 倍增找公共祖先

设要寻找公共祖先的两个点的编号分别为 xy。那么可以分为四步:

第一步:将 x 换为深度较大的一个,方便操作。

第二步:按照 2 的次幂的步数枚举,将 x 跳至与 y 同等的深度。

第三步:让 xy 共同前进,但是不能让它们跳到它们的公共祖先上。

第四步:此时,xy 都会恰恰位于它们最近公共祖先的下一层,因此只需要让它们再跳 1 步即可。

代码如下所示

cpp
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
模板题,直接上代码:
cpp
#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;
}
最近更新