【朝夕的ACM笔记】目录与索引
最近公共祖先-倍增算法
一、基本概念
最近公共祖先问题:对于给定的一颗有根树,求其两个节点的最近公共祖先。
祖先:节点本身、节点的父亲、节点父亲的父亲……都是该节点的祖先。
公共祖先:两个节点共同的祖先。
最近公共祖先(Lowest Common Ancestor)即LCA:距离根最远的公共祖先(也是深度最大的公共祖先)。
*LCA的重要性质:
①若
②若
③两点的LCA必然在两点间的最短路上,事实上两点间的最短路就是从其中一点到LCA再到另一点。故
LCA的常见求法有:倍增算法、Tarjan算法、ST表算法、树链剖分四种。
本篇介绍第一种:倍增算法。
二、算法思想
2.1 朴素算法
在讲倍增算法前,我们先考虑一下暴力的朴素算法。
首先我们知道
所以我们可以先找到深度比较大的那个点,让它先往上跳,直到两点深度相同。
接着两点一起往上跳,什么时候汇聚在一点,那点就是最近公共祖先。
但是朴素算法的时间复杂度是比较大的,最坏情况下相当于
2.2 倍增算法
倍增算法是朴素算法的改进算法。对于每个点,我们先求出它向上的第
需要注意的是,向上跳跃时,我们应当先考虑跳大的步子。
什么意思呢?
比如说我们要求一个节点向上的第13个祖先。
那我们应该先跳8步,再跳4步,再跳1步。
对于预处理:
我们设
则显然
接下来从根节点向下DFS,每跑到一个点,都可以求出这个点的前
倍增的转移方程为
由于
对于求解LCA:
第一步先统一深度:
假设
然后一起向上跳,从大的步子开始,直到汇聚在同一点。
时间复杂度:
倍增算法预处理的时间复杂度是
三、参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
vector<int>e[500005];
int dep[500005];
int fa[500005][25];
int lg[500005];
void dfs(int s,int fn)//s表示当前点,fn是当前点的父亲节点
{
fa[s][0]=fn;//第一个祖先就是本身
dep[s]=dep[fn]+1;//记录点的深度
for(int i=1;i<=lg[dep[s]]+1;i++)
fa[s][i]=fa[fa[s][i-1]][i-1];//倍增转移
for(int i=0;i<e[s].size();i++)
if(e[s][i]!=fn) dfs(e[s][i],s);//向下遍历
}
void pre(int n)//快速预处理log2
{
lg[1]=0,lg[2]=1;
for(int i=3;i<=n;i++) lg[i]=lg[i/2]+1;
}
int lca(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);//先保证x的深度大于等于y
while(dep[x]>dep[y]) x=fa[x][lg[dep[x]-dep[y]]];//统一深度
if(x==y) return x;//特判
for(int i=lg[dep[x]];i>=0;i--)//从大步开始走
if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];//一起往上跳
return fa[x][0];
}
int read()//快读,增快读入速度
{
int re=0;
char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0' && ch<='9'){re=re*10+ch-'0';ch=getchar();}
return re;
}
int main()
{
int n,m,s;//树的点数、询问次数、根节点
n=read();m=read();s=read();
for(int i=1;i<=n-1;i++)
{
int x,y;
x=read(),y=read();
e[x].push_back(y);
e[y].push_back(x);//树是无向图
}
pre(n);//预处理log2
dfs(s,0);//dfs预处理fa数组
for(int i=1;i<=m;i++)
{
int a,b;
a=read(),b=read();
printf("%dn",lca(a,b));
}
return 0;
}