最近公共祖先
最近公共祖先简称LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点所有的公共祖先里面,离根最远的那个。
Tarjan求解最近公共祖先
基本思路
1.dfs整棵树
2.每到达一个结点,先标记这个节点已被访问
3.随后遍历与该结点又访问关系的结点(记为z),若z已经被访问过,那么二者的lca就是getf(z)。
4.接着向孩子结点遍历,回溯的时候将孩子结点与当前结点用并查集合并。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
vector<int>node[N];
int f[N];
int getf(int a){
return f[a]==a?a:f[a]=getf(f[a]);
}
int n,m,s;
int book[N];
struct{
int to,next;
}e[2*N];
int head[N],cnt;
inline void add(int a,int b){
e[++cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
int ans[N];
void Tarjan(int s){
book[s]=1;
int to;
for(int i=head[s];i;i=e[i].next){
to=e[i].to;
if(book[to])ans[(i+1)/2]=getf(to);
}
for(auto i:node[s]){
if(book[i])continue;
Tarjan(i);
f[i]=s;
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
int a,b;
for(int i=1;i<=n;++i)f[i]=i;
for(int i=1;i<n;++i){
scanf("%d%d",&a,&b);
node[a].push_back(b);
node[b].push_back(a);
}
for(int i=1;i<=m;++i){
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
Tarjan(s);
for(int i=1;i<=m;++i)printf("%d\n",ans[i]);
}
倍增
倍增是对朴素算法的优化。对于要求解的两个点a,b(假设a的深度大于b),先将两个结点上升到同一高度,然后再一起向上跳,这个过程用二进制就很快了。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
const int M=5e5+10;
struct{
int to,next;
}e[2*M];
int head[N],cnt,dep[N];
int n,m,s;
inline void add(int a,int b){
e[++cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
int len;
int fa[N][30];//fa[i][j]表示i结点的2^j个祖先
void dfs(int x,int f){
fa[x][0]=f;
dep[x]=dep[f]+1;
int to;
for(int i=1;(1<<i)<=dep[x];++i)fa[x][i]=fa[fa[x][i-1]][i-1];//这是fa的转移方程
for(int i=head[x];i;i=e[i].next){
to=e[i].to;
if(to==f)continue;
dfs(to,x);
}
}
int lca(int a,int b){
if(dep[a]<dep[b])swap(a,b);
int dif=dep[a]-dep[b];
int t=0;
//printf("%d\n",dif);
while(dif){
if(dif&(1<<t))a=fa[a][t],dif-=(1<<t);
++t;
}
//printf("%d %d\n",a,b);
if(a==b)return a;
for(int i=len;i>=0;--i){//这里可以直接理解为二分查找
if(fa[a][i]!=fa[b][i]){
a=fa[a][i];
b=fa[b][i];
}
}
return fa[a][0];
}
int main(){
scanf("%d%d%d",&n,&m,&s);
int a,b;
while((1<<len)<=n)++len;
for(int i=1;i<n;++i){
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
dfs(s,0);
for(int i=1;i<=m;++i){
scanf("%d%d",&a,&b);
printf("%d\n",lca(a,b));
}
return 0;
}
基于DFS序的倍增
众所周知,DFS序可以很快的判断出两个节点的祖孙关系。
我们只需将深度大的点一直向上跳(还是基于二进制或二分的思想跳),并不断判断祖孙关系即可。
这种方法不用将两个点升到同一高度,并且常数更小。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
const int M=5e5+10;
struct{
int to,next;
}e[2*M];
int head[N],cnt,dep[N];
int n,m,s;
inline void add(int a,int b){
e[++cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
int len;
int fa[N][30];
int dfn[N],se[N],ti;
void dfs(int x,int f){
fa[x][0]=f;
dep[x]=dep[f]+1;
int to;
dfn[x]=++ti;
se[x]=1;
for(int i=1;(1<<i)<=dep[x];++i)fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=head[x];i;i=e[i].next){
to=e[i].to;
if(to==f)continue;
dfs(to,x);
se[x]+=se[to];
}
}
int lca(int a,int b){
if(dep[a]<dep[b])swap(a,b);
int dif=dep[a]-dep[b];
int t=0;
if(dfn[b]<=dfn[a]&&dfn[b]+se[b]-1>=dfn[a])return b;
for(int i=len;i>=0;--i){
int na=fa[a][i];
if(!na)continue;//这个地方小心跳穿了,因为深度是从1开始的
if(!(dfn[na]<=dfn[b]&&dfn[na]+se[na]-1>=dfn[b]))
a=na;
}
return fa[a][0];
if(a==b)return a;
}
int main(){
scanf("%d%d%d",&n,&m,&s);
int a,b;
while((1<<len)<=n)++len;
for(int i=1;i<n;++i){
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
dfs(s,0);
for(int i=1;i<=m;++i){
scanf("%d%d",&a,&b);
printf("%d\n",lca(a,b));
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】