【算法】最近公共祖先(lca)——倍增,tarjan

问题引入

给定一棵包含 n 个节点的有根无向树,节点编号互不相同,
有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。
用并查集吗?可以,但时间复杂度太高了,很容易TLE,所以我们用LCA。

介绍

最近公共祖先是什么意思呢?其实就是对于两个点x和y,如果点t满足既是x的祖先又是y的祖先,同时是离它们最近共同的祖先,那么t就是x,y的最近公共祖先。
如图:
在这里插入图片描述

a是b,c的最近公共祖先,a是p,q的最近公共祖先。

lca就是用来求两点的最近公共祖先的算法。

倍增(在线算法)

原理

类似二分,用二进制数求出两点间的lca
分两种情况:
1.两点在同一棵子树上:
如一个链表 a-b-c,其中a为根节点,求lca(a,c)
很容易得出lca(a,c)=a

2.两点不在同一棵子树上:
首先,将两点弄到同一个高度。
接着,两点同时往上跳同一高度,跳到极限(再往上一格就撞在一起)
最后,极限+1就是lca了
如图:在这里插入图片描述
求lca(g,f)
先弄到同一高度(蓝色箭头)
然后一起跳(红色箭头)
最后跳到极限,所以lca(g,f)=a

代码实现

存边

我们用邻接表存边:

struct Edge
{
	int next,t;	
}edge[N];
inline void add(int x,int y)
{
	edge[++cnt].t=y,edge[cnt].next=head[x],head[x]=cnt;
}

需要前置知识:邻接表

初始化

想要实现这个算法,首先我们要记录各个点的深度和它跳2^i步后的节点
我们用数组d表示每个节点的深度,f[i][j]表示节点i 跳2^j步后的节点位置
所以初始化如下:

void dfs(int u,int fa)//u表示当前节点,fa表示它的父亲节点
{
	f[u][0]=fa,d[u]=d[fa]+1;
	for(int i=1;i<=lg[d[u]];i++)//非常关键
		f[u][i]=f[f[u][i-1]][i-1];//意思是u的2^i祖先等于u的2^(i-1)祖先的2^(i-1)祖先
								  //因为2^i = 2^(i-1) + 2^(i-1)
	for(int i=head[u];i;i=edge[i].next)//便利u的所有相连节点
		if(edge[i].t!=fa)//如果不是父亲节点
			dfs(edge[i].t,u);//继续搜索
}
for(int i=1;i<=n;i++)//预先算出log_2(i)+1的值,用的时候直接调用就可以了
	lg[i]=lg[i-1]+(1<<lg[i-1]==i);//看不懂的可以手推一下
dfs(s,0);//s为根节点

LCA

I.深度调换

我们求lca(x,y),默认x比y要深,所以如果x没y深,将x,y交换一下:

if(d[x]<d[y])
	swap(x,y);//注意 交换x和y,不是d[x]和d[y]
II.调至同一深度

我们已经让x的深度比y大了,现在将它们调至同一深度:

while(d[x]>d[y])
	x=f[x][lg[d[x]-d[y]]-1];//手动推一下,每次都跳到准极限
III.特殊情况

现在x和y在同一深度,如果x==y,那么x就是lca(x,y)的值:

if(x==y)
	return x;
IV.一起跳

现x和y在同一深度的不同子树上,我们让它们一起跳2^i步,直到极限(上文说过):

for(int i=lg[d[x]]-1;i>=0;i--)
	if(f[x][i]!=f[y][i])//如果跳了之后不相等,就跳
		x=f[x][i],y=f[y][i];
V.返回

此时x和y都跳到了极限,所以极限+1为所求:

return f[x][0];
VI.例子
int lca(int x,int y)
{
	if(d[x]<d[y])
		swap(x,y);
	while(d[x]>d[y])
		x=f[x][lg[d[x]-d[y]]-1];
	if(x==y)
		return x;
	for(int i=lg[d[x]]-1;i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}

例题

题目

祖孙询问
给定一棵包含 n 个节点的有根无向树,节点编号互不相同,
有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。

输入格式
输入第一行包括一个整数 表示节点个数;
接下来 n 行每行一对整数 a 和 b,表示 a 和 b 之间有一条无向边。如果 b 是 −1,那么 a 就是树的根;
第 n+2 行是一个整数 m 表示询问个数;
接下来 m 行,每行两个不同的正整数 x 和 y,表示一个询问。
1≤n,m≤4×104,
1≤每个节点的编号≤4×104

输出格式
对于每一个询问,若 x 是 y 的祖先则输出 1,若 y 是 x 的祖先则输出 2,否则输出 0。

输入/输出例子1
输入:

10
234 -1
12 234
13 234
14 234
15 234
16 234
17 234
18 234
19 234
233 19
5
234 233
233 12
233 13
233 15
233 19

输出:

1
0
0
0
2
样例解释

code

#include<bits/stdc++.h>
using namespace std;
struct fy
{
	int t,next;
}edge[1000001];
int head[1000001],cnt,x,y,n,m,s,d[1000001],f[1000001][22],lg[1000001];
void add(int x,int y)
{
	edge[++cnt].t=y,edge[cnt].next=head[x],head[x]=cnt;
}
void dfs(int u,int fa)
{
	f[u][0]=fa,d[u]=d[fa]+1;
	for(int i=1;i<=lg[d[u]];i++)
		f[u][i]=f[f[u][i-1]][i-1];
	for(int i=head[u];i;i=edge[i].next)
		if(edge[i].t!=fa)
			dfs(edge[i].t,u);
}
int lca(int x,int y)
{
	if(d[x]<d[y])
		swap(x,y);
	while(d[x]>d[y])
		x=f[x][lg[d[x]-d[y]]-1];
	if(x==y)
		return x;
	for(int i=lg[d[x]]-1;i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&x,&y);
		if(y!=-1)
			add(x,y),add(y,x);
		else
			s=x;
	}
	scanf("%d",&m);
	for(int i=1;i<=n;i++)
		lg[i]=lg[i-1]+(1<<lg[i-1]==i);
	dfs(s,0);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		int z=lca(x,y);
		if(z==x)
			printf("1\n");
		else if(z==y)
			printf("2\n");
		else
			printf("0\n");
	}
	return 0;
}

总结

类似二分,O(nlogn)

Tarjan(离线算法)

原理

类似dfs,先搜一块子树,根据已知条件求答案
区别倍增:先搜集完答案信息再dfs,不是拿一个问题得出一个答案
方法:并查集

代码实现

初始化

存边
struct fy
{
	int next,t;	
}edge[N];
void add(int x,int y)
{
	edge[++cnt].t=y,edge[cnt].next=head[x],head[x]=cnt;
}
并查集
int ga(int x)
{
	if(x!=fa[x])
		fa[x]=ga(fa[x]);
	return fa[x];
}
for(int i=1;i<=n;i++)
	fa[i]=i;//并查集初始化

Tarjan

I.标记+枚举子节点
	vis[u]=1;//标记已经搜过
	for(int i=head[u];i;i=edge[i].next)//枚举全部子节点
	{
		int j=edge[i].t;
		if(!vis[j])//判断标记
			tarjan(j),fa[j]=u;
	}
II.查找答案
	for(int i=0;i<a[u].size();i++)//查找问了关于u的问题
	{
		int y=a[u][i].u,id=a[u][i].v;
		if(vis[u])//如果找过了
			b[id]=ga(y);//直接得答案
	}

例题

题目

最近公共祖先
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先

输入格式
第一行包含三个正整数 N,M,S, 分别表示树的结点个数、询问的个数和树根结点的序号。
接下来 N-1 行每行包含两个正整数 x, y ,表示 x 结点和 y 结点之间有一条直接连接的边(数据保证可以构成树)。
接下来 M 行每行包含两个正整数 a, b ,表示询问 a 结点和 b 结点的最近公共祖先。 1<=N,M<=500000

输出格式
输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。

输入/输出例子1
输入:

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

输出:

4
4
1
4
4

样例解释

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6;
struct fy
{
	int next,t;	
}edge[N];
struct ff
{
	int u,v;
};
int head[N],cnt,fa[N],n,m,s,b[N],vis[N],x,y;
vector<ff>a[N];
void add(int x,int y)
{
	edge[++cnt].t=y,edge[cnt].next=head[x],head[x]=cnt;
}
int ga(int x)
{
	if(x!=fa[x])
		fa[x]=ga(fa[x]);
	return fa[x];
}
void tarjan(int u)
{
	vis[u]=1;
	for(int i=head[u];i;i=edge[i].next)
	{
		int j=edge[i].t;
		if(!vis[j])
			tarjan(j),fa[j]=u;
	}
	for(int i=0;i<a[u].size();i++)
	{
		int y=a[u][i].u,id=a[u][i].v;
		if(vis[u])b[id]=ga(y);
	}
}
signed main()
{
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<n;i++)
		scanf("%d%d",&x,&y),add(x,y),add(y,x);
	for(int i=1;i<=m;i++)
		scanf("%d%d",&x,&y),a[x].push_back({y,i}),a[y].push_back({x,i});
	for(int i=1;i<=n;i++)
		fa[i]=i;
	tarjan(s);
	for(int i=1;i<=m;i++)
		printf("%d\n",b[i]);
} 

总结

类dfs,常数优化,不过尽量打倍增

总结

很好的一个树形结构的算法,蒟蒻可以去这里刷题:lca刷题处

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Tarjan算法是一种用于求解最近公共祖(Least Common Ancestors,LCA)问题的离线算法算法的核心思想是利用深度优先搜索(DFS)和并查集(Union Find)来解决问题。 首先,我们从根节点开始遍历每一个节点,并将节点分为三类,用st[]数组表示。0代表还未被遍历,1代表正在遍历这个点,2代表已经遍历完这个点并且回溯回来了。这样的划分有助于确定节点的最近公共祖先。 在Tarjan算法中,我们一边遍历一边回应查询。每当遍历到一个节点时,我们查找与该节点相关的所有查询。如果查询中的节点已经被遍历完(即st[]值为2),我们可以利用已经计算好的信息来计算它们的最近公共祖先最近公共祖先的距离可以通过两个节点到根节点的距离之和减去最近公共祖先节点到根节点的距离来计算。 在Tarjan算法中,我们可以通过深度优先搜索来计算dist[]数组,该数组表示每个节点到根节点的距离。我们可以利用父节点到根节点的距离加上边的权值来计算每个节点到根节点的距离。 最后,我们可以通过并查集来操作st[]数组。当遍历完一个节点的所有子树后,将子树中的节点放入该节点所在的集合。这样,每个子树的节点的最近公共祖先都是该节点。 综上所述,Tarjan算法利用DFS和并查集来求解最近公共祖先问题。它的时间复杂度为O(n+m),其中n是节点数,m是查询次数。通过该算法,我们可以高效地解决最近公共祖先的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [最近公共祖先tarjan](https://blog.csdn.net/qq_63092029/article/details/127737575)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [【模版】Tarjan离线算法最近公共祖先(LCA)](https://blog.csdn.net/weixin_43359312/article/details/100823178)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Tarjan算法求解最近公共祖先问题](https://blog.csdn.net/Yeluorag/article/details/48223375)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值