倍增求LCA

[定义]

LCA指的是最近公共祖先(Least Common Ancestors)。如图:图片来源于百度
举个例子,如果要[yào]求M和H的最近公共祖先。很明显,M的祖先有G,E,F;而H的祖先有E,F。其公共祖先是E,F。但他们的最近公共祖先是E。很简单吧:-)
好的,接下来看一道模板题。
原题链接(洛谷P3379)

[题目描述]

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

[输入格式]

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

[输出格式]

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

[输入输出样例]

Sample Input
5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

Sample Output
4
4
1
4
4

[普通暴力]

如果我们要找B,H的最近公共祖先。(还是这张图) 我们应该先找到B和H的深度,定义为dep。
发现dep[H]<dep[B],所以把B向上跳,跳到和H相同的高度,即为D(dep[D]=dep[H])。然后两点再一起向上一次一次地跳,如果他们跳到的点相同,那么这个点就是他们的最近公共祖先。
图片来源于百度
暴力虽然实现简单,但是复杂度太高,肯定不能AC。所以,我们就应该考虑如何优化。

[优化思路]

首先,我们要知道这种算法会超时的问题所在。通过分析样例,我们发现,我们找了很多与最近公共祖先无关的点(如:D,C,E),导致算法效率低下。知道这一点后,我们优化的思路就很清晰了:就是要跳尽量少的次数(跳得尽量远),选尽量少的中继点(跳得尽量远), (这不是是一个意思吗?) 但是却仍能找到最近公共祖先 (跳得尽量远)

所以,这道题便很自然地与某神奇的方法联系上了——倍增!

  • [定义]
    倍增,意思是成倍的增加增长;成倍地增长。(字典翻译)
  • [神奇之处]

这段几乎全部是搬运这位大佬的文章

上面这段必须有,我可不想被封号
好了,现在开始就是倍增时间。
从前,有两只可爱得不得了的小白兔,它想从A地去往遥远的B地。

2B小白兔: 向右边跳一步,左边跳一步,再向右边跳很多步,再……(对不起,这个太脑残了)
普通小白兔: 向右边跳一步,再跳一步,再跳一步……再跳一步,哇,到了!好开心!

显然是个人都会选择普通小白兔的做法。但是,如果这样一次一次走下去,即使这种方法所需的空间极少,但是时间消耗的太多了。这种用时间换空间的做法,在这种题中是绝对不行的。

就像物极必反一样,2B小白兔突然就变得十分聪明,成功把自己的B去掉了。它变成了一只很2的小白兔好吧其实也没什么区别。 就在所有人都再次无情嘲笑它的时候。它突然灵光一闪,他没想到,自己的名字竟有如此神奇的魔力。没错就是2!

这只聪明的兔子它开始跳一步,然后跳上一次跳的步数 * 2,这样他以比普通小白兔快无数倍的速度到了终点,鄙视一下那只一步一步跳的小白兔;

于是,这只自信的2B兔子开始自信地尝试做LCA。我绝对没有说你们

[正解]

我们采用树倍增法。不妨设f[x][k]表示x的2^k倍祖先。这样,我们查找祖先时以2的倍数递减,就能极大地提高效率。

  • 主函数
int main(void)//从主函数开始阅读是好习惯;
{
	log[0]=-1;
	scanf("%d %d %d",&n,&m,&s);
	int i,x,y;
	for(i=1;i<=n-1;i++)
	{
		log[i]=log[i>>1]+1;
		scanf("%d %d",&x,&y);
		add(x,y);//储存路径:邻接表;
		add(y,x);
	}
	dfs(s,0);//初始化;
	for(i=1;i<=m;i++)
	{
		scanf("%d %d",&x,&y);
		if(dep[x]<dep[y])//我们为了方便把要[yào]求的节点调整到同一高度,就默认设置dep[x]>dep[y];
		{
			swap(x,y);
		}
		printf("%d\n",query(x,y));//query是查找函数;
	}
	return 0;
}

log的用处(这是我自己加的,其实不要也无所谓,只是我觉得这样会快一些(真的就只能快一些) :

n(dep)log[n]
0-1
10
21
31
42
52
62

先给出公式:log[0]=-1(只是初始化);log[i]=log[i/2]+1;
很明显,log的作用是:有一点x,其深度为n,log存的是n在二进制中,最左端1的位置;也就是说,x第一次跳只能跳2^(log[n])。

  • 储存
void add(int x,int y)//因为这道题的节点数很大,所以建议用邻接表储存。当然,因为是无向图,所以每次输入要存两次;
{
	tot++;
	next[tot]=first[x];
	first[x]=tot;
	v[tot]=y;
}
  • 初始化
void dfs(int x,int father)//father是x的父节点;
{
	dep[x]=dep[father]+1;//这就没必要解释了吧;
	int i;
	for(i=1;i<=log[dep[x]];i++)//log在现在就有用了,比起蓝皮书直接把i<=log[dep[x]]换成了i<=20,确实快了一丢丢;
	{
		f[x][i]=f[f[x][i-1]][i-1];//点x直接向上跳2^i步等价于x向上跳了两次2^(i-1)步;
	}
	for(i=first[x];i;i=next[i])//找和x连通的点,找到即是x的儿子;
	{
		if(v[i]==father)
		{
			continue;
		}
		f[v[i]][0]=x;//把x的子节点的父节点设置为x;
		dfs(v[i],x);
	}
	return;
}
  • 查询
int query (int x,int y)
{
	int i;
	if(dep[x]!=dep[y])//如果初始高度不相等,就把高度高的x与相对低的y齐平;
	{
		for(i=log[dep[x]];i>=0;i--)
		{
			if(dep[f[x][i]]>=dep[y])//如果发现dep[f[x][i]]仍然大于dep[y],就把x变为f[x][i];
			{
				x=f[x][i];
			}
			if(x==y)//如果齐平后发现x=y,说明他们的最近公共祖先就是y,直接返回;
			{
				return x;
			}
		}
	}
	for(i=log[dep[x]];i>=0;i--)//高度相等后,开始找祖先(不是公共!!);
	{
		if(f[x][i]!=f[y][i])//因为是f[x][i]!=f[y][i]才跳,所以找的永远不可能是公共祖先,而是最近公共祖先的儿子;
		{
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];//因为找的是最近公共祖先的儿子,所以在跳一次就是最近公共祖先;
}
  • 完整代码
#include<cstdio>
#include<iostream>
using namespace std;
int n,m,s;
int f[1000001][40];
int log[500010],dep[1000010];
int tot,first[1000010],next[1000010],v[1000010];
void add(int x,int y)
{
	tot++;
	next[tot]=first[x];
	first[x]=tot;
	v[tot]=y;
}
void dfs(int x,int father)
{
	dep[x]=dep[father]+1;
	int i;
	for(i=1;i<=log[dep[x]];i++)
	{
		f[x][i]=f[f[x][i-1]][i-1];
	}
	for(i=first[x];i;i=next[i])
	{
		if(v[i]==father)
		{
			continue;
		}
		f[v[i]][0]=x;
		dfs(v[i],x);
	}
	return;
}
int query (int x,int y)
{
	int i;
	if(dep[x]!=dep[y])
	{
		for(i=log[dep[x]];i>=0;i--)
		{
			if(dep[f[x][i]]>=dep[y])
			{
				x=f[x][i];
			}
			if(x==y)
			{
				return x;
			}
		}
	}
	for(i=log[dep[x]];i>=0;i--)
	{
		if(f[x][i]!=f[y][i])
		{
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}
int main(void)
{
	log[0]=-1;
	scanf("%d %d %d",&n,&m,&s);
	int i,x,y;
	for(i=1;i<=n-1;i++)
	{
		log[i]=log[i>>1]+1;
		scanf("%d %d",&x,&y);
		add(x,y);
		add(y,x);
	}
	dfs(s,0);
	for(i=1;i<=m;i++)
	{
		scanf("%d %d",&x,&y);
		if(dep[x]<dep[y])
		{
			swap(x,y);
		}
		printf("%d\n",query(x,y));
	}
	return 0;
}

附赠另一道模板题Lowest Common Ancestor

<=to be continued

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值