LCA(最近公共祖先)

本文详细介绍了两种求解最近公共祖先(LCA)问题的算法:倍增法和Tarjan离线算法。倍增法采用预处理和在线查询的方式,时间复杂度分别为O(nlogn)和O(logn),适用于动态查询。Tarjan离线算法利用并查集和深度优先搜索,时间复杂度为O(n+m),适用于离线处理多个询问。两种方法各有特点,适用于不同的场景。
摘要由CSDN通过智能技术生成

LCA(最近公共祖先)

1.倍增法(在线算法)
2.Tarjan(离线算法)
注:
在线做法:读一个询问,处理一个,输出一个
离线做法:读完全部询问,再全部处理完,再全部输出


一、倍增法

1、 算法过程:

1.预处理出 fa[i,j] ,和 depth[i] 。( fa[i,j] 表示从i这个点向上跳2j到达的点,depth[i] 表示这个点在这课有根树中的深度,同时要将 depth[0] 设置为0,方便代码的书写)。

2.先将两个点跳到同一个深度。

3.让两个点同时向上跳,直到两个点跳到其最近公共最先的下一层。(如果跳到同一层的话,不能确定是否是最近的)。
4.输出fa[当前点][0]即为最近公共祖先节点。

注:通过递归的方式处理,可以先向上跳2j-1步,在向上跳2j-1步,那么这样的话,预处理是就可以写成fa[i][j]=fa[fa[i][j-1]][j-1]。向上跳的过程有点类似于快速幂,拼凑出一个十进制值等于该节点到根节点距离的二进制数,如果 j 位为1,那么就可以向上跳2j步。

2、时间复杂度

预处理:O(nlogn)

查询:O(logn)

3、模板:

参考例题(AcWing 1172. 祖孙询问 )

#include <bits/stdc++.h>
using namespace std;
const int N=40010,M=N*2;
int n,m;
struct node{
	int to,nex;
}edge[M];
int head[N],cnt;
int depth[N],fa[N][20];
int q[N];
void addedge(int u,int v)
{
	edge[cnt].to=v;
	edge[cnt].nex=head[u];
	head[u]=cnt++;
}
//预处理出depth,和fa数组 
void bfs(int root)
{
	memset(depth,0x3f,sizeof(depth));
	depth[0]=0;depth[root]=1;
	queue<int> q;
	q.push(root);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=head[u];i!=-1;i=edge[i].nex)
		{
			int v=edge[i].to;
			if(depth[v]>depth[u]+1)
			{
				depth[v]=depth[u]+1;
				q.push(v);
				fa[v][0]=u;
				for(int k=1;k<=15;k++)
					fa[v][k]=fa[fa[v][k-1]][k-1];
			}
		}
	}
}
//向上跳的实现 
int lca(int a,int b)
{
	if(depth[a]<depth[b]) swap(a,b);
	for(int k=15;k>=0;k--)
		if(depth[fa[a][k]]>=depth[b])//因为设置了depth[0]=0,所以不会跳过根节点
			a=fa[a][k];
	if(a==b) return a;
	for(int k=15;k>=0;k--)
		if(fa[a][k]!=fa[b][k])
		{
			a=fa[a][k];
			b=fa[b][k];	
		}	
	return fa[a][0];
}
int main()
{
	cin>>n;
	int root=0;
	memset(head,-1,sizeof(head));
	cnt=0;
	for(int i=1;i<=n;i++)
	{
		int u,v;
		cin>>u>>v;
		if(v==-1) root=u;
		else addedge(u,v),addedge(v,u);
	}
	bfs(root);
	
	cin>>m;
	while(m--)
	{
		int a,b;
		cin>>a>>b;
		int p=lca(a,b);
		if(p==a) cout<<1<<endl;
		else if(p==b) cout<<2<<endl;
		else cout<<0<<endl; 
	}
}

二、Tarjan离线算法

1、算法步骤:

在这里插入图片描述
主要思想:这里将整个树分为三个部分,并用st数组标记每个点的属于那个部分,st[i]==2,i这个点属于搜索且回溯过的点(绿色部分);st[i]==1,i这个点正在被搜索但未回溯;st[i]==0,i这个点还未被搜索。
通过对st值为2的点进行向上合并,就可以实现将已搜索过的点合并到正在搜索的这条链上,找到最近公共祖先。

1.建立对应的树,将所有问题存入一个结构体或者一个pair数组。
2.进行一个dfs,在回溯的时候利用并查集将节点合并到父结点上。
3.每进行一次搜索,就将问题数组遍历一遍,如果有与当前点相关的问题的时候,判断另一个点是否被搜索且回溯,如果是,则将另一个点的并查集中的值存入存放答案的对应数组位置。

2、时间复杂度:

O(n+m) n个点,m条询问

3、模板

参考例题(AcWing 1171. 距离 )

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N=10010,M=N*2;

struct node{
	int to,nex,w;
}edge[M];
int head[N],cnt;
int dis[N];
int n,m;
int p[N];
int res[M];
int st[N];
vector<PII> query[N];   // first存查询的另外一个点,second存查询编号

void addedge(int u,int v,int w)
{
	edge[cnt].to=v;
	edge[cnt].w=w;
	edge[cnt].nex=head[u];
	head[u]=cnt++;
}

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void bfs(int s)
{
	memset(dis,0x3f3f3f3f,sizeof(dis));
	queue<int> q;
	q.push(s);
	dis[s]=0;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=head[u];i!=-1;i=edge[i].nex)
		{
			int v=edge[i].to;
			if(dis[v]==0x3f3f3f3f)
			{
				dis[v]=dis[u]+edge[i].w;
				q.push(v);
			}
		}
	}
}
void tarjan(int u)
{
	
    st[u] = 1;
    for(int i=head[u];i!=-1;i=edge[i].nex)
    {
    	int v=edge[i].to;
    	if(!st[v])
    	{
    		tarjan(v);
    		p[v]=u;
		}
	}
    for(int i=0;i<query[u].size();i++)
    {
    	PII p=query[u][i];
        int y = p.first, id = p.second;
        if (st[y] == 2)
        {
            int anc=find(y);
            res[id]=dis[u]+dis[y]-dis[anc]*2;
        }
    }
    st[u] = 2;
}

int main()
{
    cin>>n>>m;
    memset(head,-1,sizeof(head));
    cnt=0;
    for(int i=1;i<=n-1;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
       	addedge(u,v,w);
       	addedge(v,u,w);
    }
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v; 
        if (u!=v)
        {
            query[u].push_back(PII(v,i));
            query[v].push_back(PII(u,i));
        }
    }
	
    for(int i=1;i<=n;i++) 
		p[i]=i;

    bfs(1);
    tarjan(1);
    for(int i=1;i<=m;i++)
    	cout<<res[i]<<endl;
    	
    return 0;
}

参考资料:
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/154795/
来源:AcWing算法提高课

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值