最近公共祖先 (LCA倍增)
题目描述:
解题思路:
首先思考暴力算法,我们先将待处理的
u
,
v
u,v
u,v 两点移动到相同深度的地方,然后两个一起向个自的父节点往上跳,直到跳到第一次父节点相同的时候这个相同的父节点即为它们的最近公共祖先。
很显然这个思想是没有问题,但是要是纯属暴力的一级一级往上跳这样实现,很显然会超时,那么我们就可以考虑倍增优化。
我们知道倍增可以把一大段繁琐的步骤压缩为一个一次就进行
2
x
2^x
2x 次的步骤进行对步骤的加速。因此我们可以把算法中的一级一级往上跳转换为每次跳
2
i
2^i
2i 级,也就是说在树上倍增找到最近公共祖先。
我们看看这棵树:
考虑寻找节点 17 和节点 18 的最近公共祖先。
我们发现 17 的深度为
6
6
6 ,而
18
18
18 的深度为
7
7
7 ,因此我们要先将
18
18
18 往上跳一级,变成
16
16
16 ,这样子两点的深度就相同了,我们就可以开始寻找最近公共祖先。
由于深度为 6 ,因此第一次尝试倍增跳的级数为
2
l
o
g
2
6
2^{log_{~2}~6}
2log 2 6 既向上跳
2
2
2^2
22 次。
然后我们发现跳到了节点 3 ,然后他们的父亲几点相同了,因此显然不能这么跳,于是我们减少跳的步数,考虑跳
2
1
2^1
21 步,这时 17跳到了 10,18 跳到了8,他们的父节点仍不相同,因此可以这么跳。
我们继续减少跳的步数,跳
2
0
2^0
20 级,他们分别跳到了7 和 5,他们的父节点相同了,也就是说7 和 5的是17 和 18的最近公共祖先。
这些跳的路径是这样的:
17
−
>
10
−
>
7
−
>
3
17->10->7->3
17−>10−>7−>3
18
−
>
16
−
>
8
−
>
5
−
>
3
18->16->8->5->3
18−>16−>8−>5−>3
我们来考虑一些细节问题,为什么要跳到两个节点不相同但是父节点相同的节点,而不是直接跳到一个相同的节点呢?因为,如果一开始跳的步数就很大,那么可能会一条就跳到类似于根节点是多个节点祖先之类的节点,这时候显然根节点是两个节点的祖先没错,但是会存在这么一种情况,就算我们一开始跳到的就是两个节点的祖先,但是我们并不能保证这个祖先是离他俩最近的。
因此我们要将步数递减,找到最后两个节点不同,但父节点相同的节点,那么这两个节点的父节点就肯定是是最近公共祖先。
正如上述步骤所说的,我们如果跳到了 2,那么2是17和18的祖先没错,但是我们无法保证它是最近的,但是我们如果跳到了 3和5,他们的父节点相同,那么可以证明3和5的父节点就是17和18的最近公共祖先。
对算法的再优化:
- 对log的优化
我们发现第一次跳的步数总是 2 l o g 2 x 2^{log_{~2}~x} 2log 2 x,那么这个 l o g 2 x log_{~2}~x log 2 x 可以怎样快速求出来呢?
考虑建立一个递推式,设
l
g
i
lg_{i}
lgi 表示
(
l
o
g
2
i
)
+
1
(log_{~2}~i)+1
(log 2 i)+1 的值,那么:
l
g
i
=
l
g
i
−
1
+
(
2
l
g
i
−
1
=
i
)
lg_{i}=lg_{i-1}+(2^{lg_{~i-1}}=i)
lgi=lgi−1+(2lg i−1=i)
意思是,若
2
l
g
i
−
1
2^{lg_{~i-1}}
2lg i−1 等于
i
i
i, 那么就加1,否则不加1,可以证明这个递推式可以正确求出所有
l
g
i
lg_i
lgi。
for(int i=1;i<=n;i++)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
- 对倍增的实现
为了实现倍增,我们显然需要每一次都快速知道节点的第 2 x 2^x 2x 级祖先是谁:
设
f
a
i
,
j
fa_{i,j}
fai,j 表示节点
i
i
i 往上的第
2
j
2^j
2j 级祖先,那么可以得到递推式:
f
a
i
,
j
=
f
a
f
a
i
,
j
−
1
,
j
−
1
fa_{i,j}=fa_{fa_{i,j-1},j-1}
fai,j=fafai,j−1,j−1
意思是 i 的第 2 j 2^j 2j 级祖先为 i 的第 2 j − 1 2^{j-1} 2j−1 级祖先的第 2 j − 1 2^{j-1} 2j−1 级祖先,他是由 2 x = 2 x − 1 + 2 x − 1 2^x=2^{x-1}+2^{x-1} 2x=2x−1+2x−1 来证明的。
fa[s][0]=pre;
for(int i=1;i<=lg[dep[s]];i++)
fa[s][i]=fa[fa[s][i-1]][i-1];
这两条递推式是非常重要的,有了这两条递推式倍增的优化才能最大限度的发挥出来。
CODE:
完整的倍增 LCA 代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,root;
vector<int> g[500010];
int lg[500010];
int dep[500010],fa[500010][50];
void LG()
{
for(int i=1;i<=n;i++) //处理好lg数组
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
}
void dfs(int s,int pre)
{
dep[s]=dep[pre]+1; //在dfs中记录每个点的深度
fa[s][0]=pre;
for(int i=1;i<=lg[dep[s]];i++)
fa[s][i]=fa[fa[s][i-1]][i-1]; //处理祖先
for(int i=0;i<g[s].size();i++)
{
if(g[s][i]==pre) continue;
dfs(g[s][i],s);
}
}
int lca(int u,int v)
{
while(dep[u]>dep[v]) u=fa[u][lg[dep[u]-dep[v]]-1];
//若u的深度大于v的深度,那么利用倍增将u移到与v同级的地方
while(dep[v]>dep[u]) v=fa[v][lg[dep[v]-dep[u]]-1];
//若v的深度大于u的深度,那么利用倍增将v移动到与u同级的地方
if(u==v) return u; //如果u和v同点,那么显然他们互为最近公共祖先
for(int i=lg[dep[u]]-1;i>=0;i--)
if(fa[u][i]!=fa[v][i])
//若父节点不相同则像上跳,最终会跳到两个点不相同,但是父节点相同的两个点上
{
u=fa[u][i];
v=fa[v][i];
}
return fa[u][0]; //返回最后相同的父节点
}
int main()
{
cin>>n>>m>>root;
int a,b;
for(int i=1;i<n;i++)
{
cin>>a>>b;
g[a].push_back(b);
g[b].push_back(a);
}
LG();
dfs(root,0);
for(int i=1;i<=m;i++)
{
cin>>a>>b;
cout<<lca(a,b)<<endl;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!