trajan算法求lca 超级详细配图讲解

关于离线算法
(下面内容可以略过。)
离线算法其实就是将多个询问一次性解决。离线算法往往是与在线算法相对的。例如求LCA的算法中,树上倍增属于在线算法,在对树进行 O ( n ) O(n) O(n)预处理后,每个询问用 O ( l o g 2 n ) O(log_2n) O(log2n)复杂度回答。而离线的Tarjan算法则是用 O ( n + q ) O(n+q) O(n+q)时间将询问一次性全部回答。

详解
下面是一棵树,我们将以这棵树为例子讲解Tarjan算法,其中0号点为根。
图片描述

假设对于这棵树的询问有4个,分别询问:
L C A ( 2 , 8 ) LCA(2,8) LCA(2,8)
L C A ( 5 , 6 ) LCA(5,6) LCA(5,6)
L C A ( 2 , 5 ) LCA(2,5) LCA(2,5)
L C A ( 4 , 9 ) LCA(4,9) LCA(4,9)
首先我们将这四个询问顺序调转,再复制四份,现在就有8个询问:
L C A ( 2 , 8 ) LCA(2,8) LCA(2,8)
L C A ( 5 , 6 ) LCA(5,6) LCA(5,6)
L C A ( 2 , 5 ) LCA(2,5) LCA(2,5)
L C A ( 4 , 9 ) LCA(4,9) LCA(4,9)
L C A ( 8 , 2 ) LCA(8,2) LCA(8,2)
L C A ( 6 , 5 ) LCA(6,5) LCA(6,5)
L C A ( 5 , 2 ) LCA(5,2) LCA(5,2)
L C A ( 9 , 4 ) LCA(9,4) LCA(9,4)
这一步是必须的,后面将会说明它。
然后对于每个节点u,给它开一个链表,找到所有的询问 L C A ( u , v ) LCA(u,v) LCA(u,v) ,把v插入到u的链表后,同时把询问编号插入,以便按照输入顺序输出答案。
于是询问就被离线了。
那么到底怎么求LCA呢?我们对带着询问树进行一次dfs。如图:
第1步,0号点被遍历:
图片描述

没有与0相关的询问,继续dfs。
第2步,1号点被遍历:
图片描述

没有与1相关的询问,继续dfs。
第3步,2号点被遍历:
图片描述

2号点没有儿子了,与2相关的询问有 L C A ( 2 , 5 ) LCA(2,5) LCA(2,5) L C A ( 2 , 8 ) LCA(2,8) LCA(2,8)
但是5号点和8号点都还没有遍历过,我们什么也不知道,因此这两个询问不理它。
第4步,2号点回溯(遍历完毕并回溯的点标为蓝色):
图片描述

第5步,3号点被遍历:
图片描述

图片描述

没3号点的事,继续dfs。
第6步,4号点被遍历:
图片描述

关于4号点的询问我们也是一无所知,回溯。
第7步,4号点回溯:
图片描述

第8步,5号点被遍历:
图片描述

关于5的询问有 L C A ( 5 , 6 ) LCA(5,6) LCA(5,6) L C A ( 5 , 2 ) LCA(5,2) LCA(5,2)
6号点的信息我们还不知道,但是2号点,我们已经知道它已经被访问且回溯了。
5的祖先一定在当前正在访问的节点中(也就是访问了还没回溯的点),那么
L C A ( 5 , 2 ) LCA(5,2) LCA(5,2) 其实也就是在图上红色的节点里找出满足如下两个条件的点:
1.它是2的祖先。
2.它深度最大。
很容易发现这个点就是1,于是这里就可以记录下来 L C A ( 5 , 2 ) = 1 LCA(5,2)=1 LCA(5,2)=1
第9步,5号点回溯:
图片描述

第10步,3号点回溯:
图片描述

第11步,6号点被遍历:
图片描述

还是跟之前一样,对于跟6号点有关的询问 L C A ( 6 , 5 ) LCA(6,5) LCA(6,5) ,去找红色点里深度最大的5的祖先,显然就是1,记下 L C A ( 6 , 5 ) = 1 LCA(6,5)=1 LCA(6,5)=1
第12步,6号点回溯。
图片描述

第13步,1号点回溯:
图片描述

第14步,7号点被遍历:
图片描述

第15步,8号点被遍历:
图片描述

按照之前做法,在红色节点里找出深度最大的2的祖先,可以求出 L C A ( 8 , 2 ) = 0 LCA(8,2)=0 LCA(8,2)=0
第16步,8号点回溯:
图片描述

第17步,9号点被遍历:
图片描述

显然了, L C A ( 9 , 4 ) = 0 LCA(9,4)=0 LCA(9,4)=0
后面的过程就略过,因为至此我们已经求出了四个询问的答案。
L C A ( 2 , 8 ) = 0 LCA(2,8)=0 LCA(2,8)=0
L C A ( 5 , 6 ) = 1 LCA(5,6)=1 LCA(5,6)=1
L C A ( 2 , 5 ) = 1 LCA(2,5)=1 LCA(2,5)=1
L C A ( 4 , 9 ) = 0 LCA(4,9)=0 LCA(4,9)=0
也许你已经明白了,为什么要把 L C A ( u , v ) LCA(u,v) LCA(u,v)复制一份 L C A ( v , u ) LCA(v,u) LCA(v,u),因为在上面过程中,我们不能保证遍历u时v已经回溯,因此需要复制一个询问。
上面的过程已经可以离线求出LCA了,但复杂度不是最优的,问题就出在上面找“红色节点中u的深度最大的祖先”,如果从u点一步步向上跳,复杂度为 O ( n q ) O(nq) O(nq)
假如对于一个询问 L C A ( u , v ) LCA(u,v) LCA(u,v),u已经被遍历过,此时遍历到v。容易发现 L C A ( u , v ) LCA(u,v) LCA(u,v)一定是红色的(也就是访问了还未回溯)。那么如果我们在dfs的过程中,在节点u的儿子遍历完毕回溯时,将儿子的fa指向点u,那么对于询问 L C A ( u , v ) LCA(u,v) LCA(u,v),只需要从u开始,不断往u的父亲跳,跳到的深度最小一个节点,就是 L C A ( u , v ) LCA(u,v) LCA(u,v)
怎么去证明呢?首先其必是u的祖先,这个不用说。但为什么是深度最小的那一个呢?不是要求深度最大的吗?因为我们是在回溯时将u的fa指向它的父亲的,如果深度不是最小,则u的这个祖先的子树里肯定没有v。如果有v的话,其必然是深度最小的那一个。由于u已访问完毕,而v还在访问中,因此u的父亲里不会有比 L C A ( u , v ) LCA(u,v) LCA(u,v) 深度更大的点,此时就能保证u的fa里深度最小的那个就是 L C A ( u , v ) LCA(u,v) LCA(u,v)
“将儿子的父亲指向点u”这个操作用并查集完成,可以保证在常数复杂度。因此对树进行遍历需要 O ( n ) O(n) O(n)复杂度,而总共有q个询问,每个询问可以 O ( 1 ) O(1) O(1)回答,复杂度为 O ( n + q ) O(n+q) O(n+q)

在此奉上代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
int ver[N],Next[N],head[N],tot,vis[N],f[N],ans[N],n,m,root;
struct aaa{
	int id,y;
};
vector < aaa > q[N];
void add(int x,int y)
{
	ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
int get(int x)
{
	if(x==f[x]) return x;
	return f[x]=get(f[x]);
}
void trajan(int x)
{
	vis[x]=1;
	for(int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if(vis[y]) continue;
		trajan(y);
		f[y]=x;
	}
	for(int i=0;i<q[x].size();i++)
	{
		int id=q[x][i].id,y=q[x][i].y;
		if(vis[y]==2) ans[id]=get(y);
	}
	vis[x]=2;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>root;
	for(int i=1;i<=n;i++) f[i]=i;
	int x,y;
	for(int i=1;i<n;i++) cin>>x>>y,add(x,y),add(y,x);
	for(int i=1;i<=m;i++)
	{
		cin>>x>>y;
		if(x==y)
		{
			ans[i]=x;
			continue;
		} 
		q[x].push_back({i,y}),q[y].push_back({i,x});
	}
	trajan(root);
	for(int i=1;i<=m;i++)
		cout<<ans[i]<<'\n';
	return 0;
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值