tarjan求LCA
最近又重新回顾了下tarjan离线算法求LCA,算是明白是什么意思了,在博客园发现很多文章并没有图,所以这里画个图来帮助还没有理解的人,也算是自己巩固下
LCA
首先我们还是来回顾下什么是LCA
就是最近公共祖先,即a,b的最近公共祖先既是a的祖先,也是b的祖先,况且是a,b的所有公共祖先里面离a和b最近的
在上面这个图中,2和5的公共祖先有3和1,但是LCA是3,因为3比1更接近3和5
Tarjan算法
tarjan算法就这几个步骤:
1.标记当前节点x已访问,但是还没有回溯
2.枚举所有没有访问的过的子节点y,继续遍历子节点
3.合并y到x上
4.访问所有与当前节点有询问并且回溯过的y x,y的lca就是find(y)
5.标记当前节点回溯
在合并的过程中,我们用到了并查集这个数据结构
如果你看不懂,我们来结合代码:
vector<int>g[N];
vector<pair<int,int>>query[N];
int find(int x) //并查集的find函数
{
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void tarjan(int q) //q表示当前遍历的节点
{
vis[q]++; //标记为1
for(int t:g[q]) //枚举子节点
{
if(!vis[t]) //如果子节点还没有被访问
{
tarjan(t); //遍历子节点
fa[t] = q; //合并子节点到当前节点
}
}
for(auto t:query[q])//与q结点有关的询问t.second是问题编号,t.first是询问的另一个结点
{
int v = t.x, id = t.y;
if(vis[v] == 2 && !lca[id]) //如果第id个问题还没有找到lca并且v节点已回溯
{
lca[id] = find(v);
}
}
vis[q]++; //标记为2 表示已回溯
}
其实我想这段伪代码对于大多数acmer来说很好实现,可是大家可能还是不清楚这个到底是怎么回事
首先大家要明白一件事:两个点的lca要么是其中一个点,要么就是其父亲及其祖先了
还是刚刚那张图,我们查询2和8的lca
红色->橙色->蓝色->绿色为dfs遍历的顺序
首先我们让所有fa[i]=i 其实就是先假设lca可能就是自己
我们从根节点1遍历到3,先到2,发现2是子节点不能往下走了,然后看和2有关的查询5节点,发现5根本就没有访问过,所以只能回溯了,那么lca为2是不可能的了,所以把2合并到3里面,因为和2有关的lca不能是2了,因为3是2的父亲,所以3是极有可能作为lca的
这就是为什么我们有
fa[t] = q;
因为t不可能作为lca了,但是q有可能。
我们继续遍历5,8 然后遍历到8发现和8有关的2已经回溯过了,所以2和8的lca是find(2)也就是3.
假如我们要查询2和4也是同理。但我们从3回溯以后
fa[3]已经变成1了。为什么呢?
因为3已经不可能再作为剩下的点的lca了,能把3当做lca的点已经在遍历3的子节点的时候已经遍历过了
换个角度看,tarjan算法也可以理解为,我一个一个的看子树,我先看5的子树里面有没有以5为lca的,如果没有,那就看3的子树里面有没有以3为lca的。以此类推。
洛谷模板题:https://www.luogu.com.cn/problem/P3379
代码如下:
#include<iostream>
#include<algorithm>
#include<vector>
#define x first
#define y second
using namespace std;
typedef unsigned long long ll;
const int N = 5e+5+100;
typedef pair<int,int> PII;
int n,m,vis[N],fa[N],lca[N];
vector<int>g[N];
vector<PII>query[N];
ll read()
{
ll x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
return x*f;
}
int find(int x) //并查集的find函数
{
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void tarjan(int q) //q表示当前遍历的节点
{
vis[q]++; //标记为1
for(int t:g[q]) //枚举子节点
{
if(!vis[t]) //如果子节点还没有被访问
{
tarjan(t); //遍历子节点
fa[t] = q; //合并子节点到当前节点
}
}
for(auto t:query[q])//与q结点有关的询问t.second是问题编号,t.first是询问的另一个结点
{
int v = t.x, id = t.y;
if(vis[v] == 2 && !lca[id]) //如果第id个问题还没有找到lca并且v节点已回溯
{
lca[id] = find(v);
}
}
vis[q]++; //标记为2 表示已回溯
}
int main()
{
int s;
cin >> n >> m >> s;
for(int i = 1; i < n; i++)
{
int x,y;
scanf("%d%d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
for(int i = 1; i <= n; i++) fa[i] = i;
for(int i = 1; i <= m; i++)
{
int a,b;
scanf("%d%d",&a,&b);
if(a == b)
{
lca[i] = a;
continue;
}
query[a].push_back({b,i});
query[b].push_back({a,i});
}
tarjan(s);
for(int i = 1; i <= m; i++)
{
printf("%d\n",lca[i]);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具