【Luogu P3379】LCA问题的倍增解法

Luogu P3379
题意:对于两个节点,寻找他们的最近公共祖先。
一个显而易见的解法是对于每一个节点我们都往上遍历一遍,记录下它每一个祖先,然后再从另一个节点出发,一步一步往上走,找到以前记录过第一个节点就是这两个节点的LCA
事实上在这样的数据规模下,这种解法的时间复杂度是让人无法接受的。
很容易发现,这样的解法慢在两个节点是一步一步往上走的,也许可以想个办法一次跳一大步?
于是我们引入了倍增法求LCA。
我们知道,对于任意一个整数数,都可以使用\(2^k+2^{k-1}+...+2^0\)来表示,这是不需要证明的,因为二进制能表示十进制的每一个数。
那么我们就可以每一次跳\(2^k\)步来优化这个算法。
还有一个问题就是:我们应该从大的开始跳还是从小的开始跳呢?
答案是从大的开始,因为如果从小的开始,就有可能出现需要回溯的情况。
举个例子,如果从小的开始,我们就会凑出\(5=1+2+...\),发现不对,然后回溯,凑出\(5=1+4\),但是如果从大的开始就可以直接凑\(5=4+1\),只要判断一下加了这个数比要求的数要大就可以不要这个,取更小的。

但是我们会发现在做这件事之前,我们必须要知道两个节点的深度以便跳步,并且还需要预处理每一个节点的\(2^k\) 级的祖先。
一个DFS就可以轻松完成这个任务。

void add(int sta,int to)
{
	//链式前向星存树
	edge[++cnt].to=to;
	edge[cnt].next=head[sta];
	head[sta]=cnt;
	//head[i]表示以sta为起点的最后一条边的编号 
	//edge[i].next表示与它起点相同的上一条边的编号 
}//如果没有看懂可以寻找其他博主的资料
void dfs(int fa,int now)
{
	deepth[now]=deepth[fa]+1;//记录深度 
	ant[now][0]=fa;//ant[now][i]表示now节点的2^i级祖先 
	for (int i=1;(1<<i)<=deepth[now];i++)
		ant[now][i]=ant[ant[now][i-1]][i-1];
	//关键的一步:now的2^i级祖先就是now的2^(i-1)级祖先的2^(i-1)级祖先
	//理由如下:2^(i-1)+2^(i-1)=2*2^(i-1)=2^i 
	for (int i=head[now];i!=0;i=edge[i].next)
		if (edge[i].to!=fa) dfs(now,edge[i].to); 
		//判断用于防止往回走。 
}

预处理完之后我们就可以开始寻找LCA了,为了方便,我们采用的方法是令x,y节点先跳到同一深度,再统一往上跳。

int lca(int x,int y)
{
	if (deepth[x]<deepth[y])
		swap(x,y);//方便操作,令x为深度较大的那一个 
	while (deepth[x]>deepth[y])
		x=ant[x][lg[deepth[x]-deepth[y]]-1];
	//这样最后一定会跳到同一深度,想一想为什么。 
	//lg[i]数组表示log(2,i)+1 
	if (x==y) return x; 
	for (int i=lg[deepth[x]];i>=0;i--)//开始往上跳 
		if (ant[x][i]!=ant[y][i]) x=ant[x][i],y=ant[y][i];
	//值得注意的时,我们并不能直接跳到他们的公共祖先上,否则可能会导致误判。
	//如果直接往上跳到公共祖先,可能会多跳了。 
	return ant[x][0];
}

最后是完整代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
struct data
{
	int to,next;
}edge[500005<<1];
int cnt,head[500005],deepth[500005],ant[500005][21],lg[500005],n,m,s,x,y; 
void add(int sta,int to)
{
	//链式前向星存树
	edge[++cnt].to=to;
	edge[cnt].next=head[sta];
	head[sta]=cnt;
	//head[i]表示以sta为起点的最后一条边的编号 
	//edge[i].next表示与它起点相同的上一条边的编号 
}
void dfs(int fa,int now)
{
	deepth[now]=deepth[fa]+1;//记录深度 
	ant[now][0]=fa;//ant[now][i]表示now节点的2^i级祖先 
	for (int i=1;(1<<i)<=deepth[now];i++)
		ant[now][i]=ant[ant[now][i-1]][i-1];
	//关键的一步:now的2^i级祖先就是now的2^(i-1)级祖先的2^(i-1)级祖先
	//理由如下:2^(i-1)+2^(i-1)=2*2^(i-1)=2^i 
	for (int i=head[now];i!=0;i=edge[i].next)
		if (edge[i].to!=fa) dfs(now,edge[i].to); 
		//判断用于防止往回走。 
}
int lca(int x,int y)
{
	if (deepth[x]<deepth[y])
		swap(x,y);//方便操作,令x为深度较大的那一个 
	while (deepth[x]>deepth[y])
		x=ant[x][lg[deepth[x]-deepth[y]]-1];
	//这样最后一定会跳到同一深度,想一想为什么。 
	//lg[i]数组表示log(2,i)+1 
	if (x==y) return x; 
	for (int i=lg[deepth[x]];i>=0;i--)//开始往上跳 
		if (ant[x][i]!=ant[y][i]) x=ant[x][i],y=ant[y][i];
	//值得注意的时,我们并不能直接跳到他们的公共祖先上,否则可能会导致误判。
	//如果直接往上跳到公共祖先,可能会多跳了。 
	return ant[x][0];
}
int 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);//暂时不确定父子关系,所以要存两条边。 
	}
	dfs(0,s);
	for (int i=1;i<=n;i++) lg[i]=lg[i-1]+(1<<lg[i-1]==i);
	//用于求出log(2,i)+1的近似值,作常数优化 
	for (int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
} 
posted @ 2019-11-05 07:46  Nanjo  阅读(107)  评论(0编辑  收藏  举报