最近公共祖先(LCA)

问题引入

  已知一棵含有 n n n 个结点的树,现在要求其中编号为 a a a b b b 的两个结点的最近公共祖先(Least Common Ancestor)。应该怎么办?

思路

朴素算法

  我们一般能够想到的是这样一种算法:先把两个结点移动到同一层,然后两个结点再同时往上移动。
  举个例子,结点 a a a 在第 5 5 5 层,结点 b b b 在第 9 9 9 层。我们不难想到,这两个结点的最近公共祖先只有可能出现在第 5 5 5 层及以上的层数。所以我们首先做的事情是:对 b b b 进行 4 4 4 次求祖先操作(目的是到达和 a a a 同一层的祖先结点),得到处于第 5 5 5 层的结点 c c c。那么显然有 LCA(a,b) == LCA(a,c)随后我们做的事情是:同时对 a a a c c c 进行求祖先操作,直到它们的祖先是同一个结点。
  上面的朴素算法的时间复杂度是 O ( n ) O(n) O(n),因为两步的时间复杂度均为 O ( n ) O(n) O(n)。有没有更快的算法?

倍增算法

  求解 LCA 问题的一个快速算法是倍增算法,时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)。这种方法需要构造一个数组 f [ i ] [ j ] f[i][j] f[i][j],它表示编号为 j j j 的结点往祖先方向走 2 i 2^i 2i 得到的结点编号。数组的初始化可以利用下面的状态转移方程:
f [ i ] [ j ] = f [ i − 1 ] [ f [ i − 1 ] [ j ] ] (1) f[i][j]=f[i-1][f[i-1][j]]\tag 1 f[i][j]=f[i1][f[i1][j]](1)

  它的含义显而易见,从 j j j 往祖先方向先走 2 i − 1 2^{i-1} 2i1 步,再走 2 i − 1 2^{i-1} 2i1 步,等同于直接往祖先方向走 2 i 2^i 2i 步。
  我们的树含有 n n n 个节点,那么数组 f f f 的第 2 2 2 维大小就是 n n n。一个结点往祖先方向,最多走 n n n 步,所以数组的第 1 1 1 唯可以是 ⌈ log ⁡ 2 n ⌉ \lceil \log_2n\rceil log2n。这下我们再考虑具体算法:
  首先,我们仍然是将 a a a b b b 统一到一层。考虑利用现有的 f f f 数组,每次走的步数都是 2 2 2 的幂次方倍(这也是算法叫做倍增算法的原因),因此将相差的层数拆解为 2 2 2 的幂次方的和。
  比如, a a a 在第 4 4 4 层,而 b b b 在第 114 114 114 层。 b b b 需要往祖先方向走 110 110 110 层。那么我们有如下拆解: 110 = 2 1 + 2 2 + 2 3 + 2 5 + 2 6 110=2^1+2^2+2^3+2^5+2^6 110=21+22+23+25+26。因此,只需要五次操作,即可让 b b b 到第 4 4 4 层:

b = f[1][b];
b = f[2][b];
b = f[3][b];
b = f[5][b];
b = f[6][b];

  如果要向上走 k k k 层,由于 k k k 最多有 ⌈ log ⁡ 2 k ⌉ \lceil\log_2k\rceil log2k 个二进制位,所以最多需要借助 f f f 数组进行 ⌈ log ⁡ k ⌉ \lceil\log k\rceil logk 次操作。由于最多有 n n n 层,所以这一步的时间复杂度是 o ( log ⁡ n ) o(\log n) o(logn)
  其次,让 a a a b b b 一起往上走。对于一个 k = ⌈ log ⁡ 2 n ⌉ k = \lceil\log_2 n\rceil k=log2n,进行如下伪代码操作:

while(true){
	while(k >=0 && f[k][a] == f[k][b])
		k--;
	if(k == -1){
		a 和 b是同一个父亲的两个儿子,f[0][a] 或者 f[0][b] 都是 LCA(a,b);
		return;
	}
	a = f[k][a];
	b = f[k][b];
}

  显然最外层的循环最多执行 k k k 次左右,所以可以在 O ( k ) O(k) O(k) 的时间复杂度内完成 LCA 的查找,也即时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)
  所以,倍增算法初始化 f f f 数组的时间复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn),进行一次查询的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)。当查询次数较多时(例如达到了 n n n 的数量级),倍增算法的效率要远远由于朴素算法。

样例代码

  针对这个模版,可以参考下面的代码:

#include<iostream>

using namespace std;

int n,m,s,ecnt=1,fedge[500005],ledge[500005],f[20][500005],layer[500005];
struct {
	int end,next;
}edge[1000005];

void buildarc(int begin,int end){
	if(!begin)
		return;
	if(!fedge[begin])
		fedge[begin]=ledge[begin]=ecnt;
	else{
		edge[ledge[begin]].next=ecnt;
		ledge[begin]=ecnt;
	}
	edge[ecnt++].end=end;
}

void dfs(int cur,int l,int fa){
	layer[cur]=l;
	for(int e=fedge[cur];e;e=edge[e].next){
		if(edge[e].end==fa)
			continue;
		f[0][edge[e].end]=cur;
		dfs(edge[e].end,l+1,cur);
	}
}

void look(int a,int b){
	int temp=layer[b]-layer[a];
	for(int base=0;temp;base++,temp/=2){
		if(temp%2)
			b=f[base][b];
	}
	if(a==b){
		printf("%d\n",a);
		return;
	}
	temp = 19;
	while(true){
		while(temp>=0 && f[temp][a]==f[temp][b])
		temp--;
		if(temp==-1){
			printf("%d\n",f[0][a]);
			return;
		}
		a=f[temp][a];
		b=f[temp][b];
	}
}

int main(){
	int u,v;
	cin>>n>>m>>s;
	for(int i=1;i<n;i++){
		scanf("%d%d",&u,&v);
		buildarc(u,v);
		buildarc(v,u);
	}
	dfs(s,1,0);
	for(int i=1;i<=19;i++)
		for(int j=1;j<=n;j++){
				f[i][j]=f[i-1][f[i-1][j]];
		}
	for(int i=1;i<=m;i++){
		scanf("%d%d",&u,&v);
		if(layer[u]>layer[v])
			look(v,u);
		else
			look(u,v);
	}
	return 0;
} 

  顺便说明一下,把数组 f f f 的小维放在第一维,可以有效减少 cache miss 的几率,提高程序运行效率。

  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值