LCA 三种 解决方法讲解 (附加例题)

  LCALeast Common Ancestors

  即最近公共祖先,是指这样一个问题:在有根树中,找出某两个结点uv最近的公共祖先(另一种说法,离树根最远的公共祖先)。

一、在线算法ST算法

    所谓在线即输入一个询问,要立即返回答案,才可进行下一次询问。

   基础:dp(rmq)

   时间复杂度O(nlogn+m+n)

 步骤:

1.将树看作一个无向图,从根节点开始深搜,得到一个遍历序列。

eg.


(1)深搜节点序列:1 3 1 2 5 7 5 6 5 2 4 2 1

(2)各点深度:    1 2 1 2 3 4 3 4 3 2 3 2 1

(3)第一次出现的下标: 1 4 2 11 5 8 6 

2.x~y区间中利用RMQ算法找到深度最小返回其下标。

Eg.46的最近公共祖先

通过上一步求解我们知道它们在深搜序列中出现在8~11,即6,5,2,4

这时候用到RMQ算法,维护一个dp数组保存其区间深度最小的下标,查找时返回即可。例子中我们找到深度最小的数为2,返回其下标10

例题:

给你一棵有根树,要求你计算出m对结点的最近公共祖先。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define N 200005
using namespace std;
int tot,head[N],ver[2*N],r[2*N],first[N],dp[2*N][18];
int n,m;
bool vis[N];
struct Node{
	int to,next;
}e[2*N];

void insert(int x,int y)
{
	e[++tot].to=y;
	e[tot].next=head[x];
	head[x]=tot;
}

void dfs(int u,int dep)
{
	vis[u]=true;ver[++tot]=u;first[u]=tot;r[tot]=dep;
	for(int k=head[u];k!=-1;k=e[k].next)
	 if(!vis[e[k].to])
	 {
	 	int v=e[k].to;
	 	dfs(v,dep+1);
	 	ver[++tot]=u;r[tot]=dep;
	 }
}

void ST(int len)
{
	for(int i=1;i<=len;i++)
	  dp[i][0]=i;
	for(int j=1;(1<<j)<=len;j++)
	  for(int i=1;i+(1<<j)-1<=len;i++)
	  {
	  	int a=dp[i][j-1],b=dp[i+(1<<j-1)][j-1];
	  	dp[i][j]=r[a]<=r[b]?a:b;
	  }
}

int RMQ(int x,int y)
{
	int k=trunc(log2(y-x+1));
    int a=dp[x][k],b=dp[y-(1<<k)+1][k];
	return r[a]<=r[b] ? a:b;
}
int LCA(int u,int v)
{
	int x=first[u],y=first[v];
	if(x>y)swap(x,y);
	if(x==y)return ver[x];
	int res=RMQ(x,y);
	return ver[res];
}
int main()
{
	scanf("%d%d",&n,&m);
    memset(vis,false,sizeof(vis));
    memset(head,-1,sizeof(head));
    tot=0;
    int x,y,root;
    
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		insert(x,y);
	    vis[y]=1;
	}
	for(int i=1;i<=n;i++)
	 if(!vis[i]){root=i;break;}
	
	memset(vis,false,sizeof(vis));
	tot=0;
	dfs(root,0);

	ST(2*n-1);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		printf("%d\n",LCA(x,y));
	}
}

二、 倍增法求LCA(在线)

  又称作爬树法。

      时间复杂度:预处理O(nlogn),每次询问O(logn)

      基础:dp,求节点在树中的深度。

步骤:

1.添加边,并预处理p数组。

void addedge(int x,int y)
{
e[++tot].v=y;
e[tot].next=head[x];
head[x]=tot;
}

p[i][j]表示i的2^j倍祖先。

p[i][j]=prt[i],j=0

            p[p[i][j-1]][j-1],j>0

2.求每个点在树中的深度d[i]。

3.对于每个询问a,b

  首先判断d[a]<d[b],若小于则将a,b互换,即保证a的深度大于等于b。

  将a 的深度不断降低,调到与b相同的深度。

  这时将a,b同时调整,直到两个变量的父亲相同,即当p[a][i]!=p[b][i],则a=p[a][i],b=p[b][i],i--。

  最后p[a][0]或p[b][0]为答案。

eg.


对于上面的一棵树,我们要询问5和9的最近公共祖先

首先预处理出p[5][0]=3,p[5][1]=1;

                        p[9][0]=7;p[9][1]=4;

                       d[5]=3;d[9]=4;

然后将9调至与5同深度。

int k=trunc(log2(4));
for(int i=k;i>=0;i--)
if(d[a]-(1<<i)>=d[b])a=p[a][i];

      9->7。

接下来将5,7同时向上调整,直到它们的父亲相同,即变为3,4.。

输出3或4的父亲,即1。

例题:

给你一棵有根树,要求你计算出 m 对结点的最近公共祖先。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define N 200005
using namespace std;
struct Node{
	int v,next;
}e[N];
int n,m,tot;
int head[N],d[N];
bool vis[N];
int par[N][20];
void addedge(int x,int y)
{
	e[++tot].v=y;
	e[tot].next=head[x];
	head[x]=tot;
}
void dfs(int u,int dep)
{
	vis[u]=true;d[u]=dep;
	for(int k=head[u];k;k=e[k].next)
	  if(!vis[e[k].v])
	  {
	  	int v=e[k].v;
	  	dfs(v,dep+1);
	  }
}
void prepare()
{
	for(int j=1;(1<<j)<=n;j++)
	  for(int i=1;i<=n;i++)
	  if(par[i][j-1]!=-1)
	    par[i][j]=par[par[i][j-1]][j-1];     
}
int lca(int a,int b)
{
	if(d[a]<d[b])swap(a,b);
	int k=trunc(log2(d[a]));
	for(int i=k;i>=0;i--)
	 if(d[a]-(1<<i)>=d[b])a=par[a][i];
	if(a==b)return a;
	for(int i=k;i>=0;i--)
	{
		if(par[a][i]!=-1&&par[a][i]!=par[b][i])
		   a=par[a][i],b=par[b][i];
	}
	return par[a][0];
}
int main()
{
	scanf("%d%d",&n,&m);
	tot=0;
	memset(par,-1,sizeof(par));
	memset(vis,0,sizeof(vis));
	int x,y;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		addedge(x,y);
		par[y][0]=x;
		vis[y]=1;
	}
	int root;
	for(int i=1;i<=n;i++)
	if(!vis[i]){root=i;break;}
	memset(vis,0,sizeof(vis));
    dfs(root,0);
    prepare();
    for(int i=1;i<=m;i++)
    {
    	scanf("%d%d",&x,&y);
    	printf("%d\n",lca(x,y));
    }
    return 0;
}

例题:

给定一个包含n个节点的树,节点编号为1..n。其中,节点1为树根。

你的任务是给定这棵树的两个节点,快速计算出他们公共祖先的个数。

(第一行一个整数n1n50,000),表示树的节点个数。接下来的n行,第i行表示节点i的信息。第i行第一个数字k,表示节点i拥有孩子的个数,接着k个数字,表示这个节点所拥有的孩子的编号。如果k=0,表示该节点是叶节点。注意,我们假定节点是节点本身的祖先。n+2行是一个整数m(1m30,000),表示有m个查询。接下去m行,每行两个数字xy,表示该查询的两个节点的编号。)

两个节点的公共祖先的个数即它们的最近公共祖先的深度,因为显然最近公共祖先以上都为两节点的公共祖先。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define N 50010
using namespace std;
int n,q,tot;
struct Node{
	int u,v,next;
}e[N];
int head[N],d[N];
bool vis[N];
int p[N][20];
void addedge(int u,int v)
{
	e[++tot].u=u;e[tot].v=v;
	e[tot].next=head[u];
	head[u]=tot;
}
void dfs(int x,int dep)
{
	vis[x]=1;d[x]=dep;
	for(int i=head[x];i!=-1;i=e[i].next)
    if(!vis[e[i].v])
	  dfs(e[i].v,dep+1);
}
void prepare()
{
	for(int j=1;(1<<j)<=n;j++)
	  for(int i=1;i<=n;i++)
	  if(p[i][j-1]!=-1)
	    p[i][j]=p[p[i][j-1]][j-1];
}
int lca(int a,int b)
{
	if(d[a]<d[b])swap(a,b);
	int k=trunc(log2(d[a]));
	for(int i=k;i>=0;i--)
	 if(d[a]-(1<<i)>=d[b])a=p[a][i];
	if(a==b)return a;
	for(int i=k;i>=0;i--)
	{
		if(p[a][i]!=-1&&p[a][i]!=p[b][i])
		 a=p[a][i],b=p[b][i];
	}
	return p[a][0];
}
int main()
{
	scanf("%d",&n);
	memset(head,-1,sizeof(head));
	memset(p,-1,sizeof(p));
	tot=0;
	for(int i=1;i<=n;i++)
	{
		int k,v;
		scanf("%d",&k);
		for(int j=1;j<=k;j++)
		{
	        scanf("%d",&v);
	        addedge(i,v);
	        p[v][0]=i;
		}
	}
	memset(vis,false,sizeof(vis));
	dfs(1,1);
	
	scanf("%d",&q);
	prepare();
	for(int i=1;i<=q;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		printf("%d\n",d[lca(u,v)]);
	}
	return 0;
}
三、 离线Tarjan求LCA

所谓离线是指在读取完全部的询问后再统一处理的算法。

基础:深度优先搜索的思想,并查集

时间复杂度:O(n+q)

    基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。

   补充:上面提到的询问(x,y)中,y是已处理过的结点。那么,如果y尚未处理怎么办?其实很简单,只要在询问列表中加入两个询问(x, y)(y,x),那么就可以保证这两个询问有且仅有一个被处理了(暂时无法处理的那个就pass掉)。而形如(x,x)的询问则根本不必存储。

(1)读入数据,建立树结构,并记录下询问序列Q[],若有(u,v)的询问,则(u,v)和(v,u)都要记录。
(2)Tarjan(x)算法
   ①建立集合,自己为自己的父亲prt[x]=x;
   ②对当前节点x的每个儿子节点y进行深搜,并prt[y]=x;
   ③设置访问标记mark[x]=1,查找所有与x有关的回答,若另一点已经访问了,则另一个点的祖先就是他们的最经公共祖先。
(3)输出答案;

例题:

USACO2004 FEB】距离查询

读入一棵无根树,求树上两点的最短距离。

因为是无根树,我们不妨设1为根,用d[i]表示点i到根的距离,求树上两点距离即求两点的LCA,用d[a]+d[b]-2*d[lca(a,b)]即可算出答案。

#include<iostream>
#include<cstdio>
#include<cstring>
#define N 40010
#define M 10010
using namespace std;
int head[N],_head[N];
struct Node{
	int u,v,w,next;
}e[2*N];
struct ask{
	int u,v,lca,next;
}ea[2*M];
int dir[N],fa[N],ance[N];
bool vis[N];
void add_edge(int u,int v,int w,int &k)
{
	e[k].u=u;e[k].v=v;e[k].w=w;
	e[k].next=head[u];
	head[u]=k++;
	
	e[k].u=v;e[k].v=u;e[k].w=w;
	e[k].next=head[v];
	head[v]=k++;
}
void add_ask(int u,int v,int &k)
{
	ea[k].u=u;ea[k].v=v;ea[k].lca=-1;
	ea[k].next=_head[u];_head[u]=k++;
	
    ea[k].u=v;ea[k].v=u;ea[k].lca=-1;
	ea[k].next=_head[v];_head[v]=k++;
}
int find(int x)
{
	return x==fa[x]?x:fa[x]=find(fa[x]);
}
void uni(int x,int y)
{
	int a=find(x);
	int b=find(y);
	fa[b]=a;
}
void tarjan(int u)
{
	vis[u]=true;
	fa[u]=u;
	for(int k=head[u];k!=-1;k=e[k].next)
	 if(!vis[e[k].v])
	 {
	 	int v=e[k].v,w=e[k].w;
	 	dir[v]=dir[u]+w;
	 	tarjan(v);
	 	uni(u,v);
	 }
	for(int k=_head[u];k!=-1;k=ea[k].next)
	 if(vis[ea[k].v])
	 {
	 	int v=ea[k].v;
	 	ea[k].lca=ea[k^1].lca=find(v);
	 }
	
}
int main()
{
    int n,q,m,tot;
    char h;
	scanf("%d%d",&n,&m);
	memset(head,-1,sizeof(head));
	memset(_head,-1,sizeof(_head));
	tot=0;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		scanf("%d%d%d %c",&u,&v,&w,&h);
		add_edge(u,v,w,tot);
	}

	scanf("%d",&q);
	tot=0;
	for(int i=1;i<=q;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add_ask(u,v,tot);
	}
	
	memset(vis,0,sizeof(vis));
	dir[1]=0;
	tarjan(1);
		
	for(int i=1;i<=q;i++)
	{
		int s=i*2-1,u=ea[s].u,v=ea[s].v,lca=ea[s].lca; 
		printf("%d\n",dir[u]+dir[v]-2*dir[lca]);
	}
	
	return 0;
} 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值