[LCA] 最近公共祖先

题目描述

给你一棵有根树,要求你计算出指定两个结点的最近公共祖先。

输入格式

第一行为结点个数 n n n,结点编号为 1 1 1 n n n
接下来 n − 1 n − 1 n1 行,每行两个整数,表示第一个结点是第二个结点的父亲。
接下来的一行为两个整数 a a a b b b,要求计算出结点 a a a b b b 的最近公共祖先。

输出格式

一行一个整数为最近公共祖先的编号。

样例

样例输入1:

5
2 3
3 4
3 1
1 5
3 5

样例输出1:

3

数据范围

2 ≤ n ≤ 5 × 1 0 5 2 \le n \le 5 \times 10^5 2n5×105

题解

这是一道经典的模板题。

如果我们直接去暴力枚举每个点进行判断,复杂度很高。

改进算法,从两个点开始,直接向上跳,如果跳到相等的数,就碰到祖先了。
先用 dfs 将两个点的深度求出来,跳到同一高度(如果两个点不在同一深度,不可能相等)。
如果在这个深度两个点已经相等,此时该点为最近公共祖先。
接下来向上跳,跳到碰到相等的数停下,此时该点就为最近公共祖先。
单次询问复杂度为 Θ ( d e p ) \Theta(dep) Θ(dep) d e p dep dep 为深度)。

int t1 = dep[x], t2 = dep[y];//x, y 的深度
if(t1 < t2){//保持 t1 深度大
	swap(t1, t2);
	swap(x, y);
}
//跳到同一高度
while(t1 > t2){
	x = fa[x];
	-- t1;
}
if(x == y){
	printf("%d", x);
	continue;
}
while(t1){
	x = fa[x];
	y = fa[y];
	if(x == y){//相等,找到了最近公共祖先
		printf("%d\n", x);
		break;
	}
	-- t1;
}

考虑使用倍增进行优化。
预处理:
f i , j f_{i, j} fi,j 表示第 i i i 个点,向上跳 2 j 2^j 2j 步,来到第 f i , j f_{i, j} fi,j 个点。
显然, f i , 0 f_{i, 0} fi,0 就是第 i i i 个点的父亲。
接下来进行合并, f i , j = f [ f i , j − 1 , j ] f_{i, j} = f[f_{i, j - 1}, j] fi,j=f[fi,j1,j](从第 i i i 个点,先跳 2 j − 1 2^{j - 1} 2j1 步,再跳 2 j − 1 2^{j - 1} 2j1 步就相当于跳 2 j 2^j 2j 步)。

for(int i = 1; i <= 25; ++ i){//跳 2^i 步
	for(int j = 1; j <= n; ++ j){//第 j 个点
		f[i][j] = f[f[i][j - 1][j - 1];
	}
}

查询:

  1. 跳到同一深度。
    先保持 d e p x > d e p y dep_x > dep_y depx>depy
    如果 d e p x − d e p y dep_x - dep_y depxdepy 的第 i i i 位为 1 1 1(二进制),则 x x x 向上跳 2 i 2^i 2i 步。
int tt = t1 - t2;//深度差
for(int i = 0; i <= 25; ++ i){
	if(tt & 1){//第 i 位为 1
		x = f[x][i];
	}
	tt /= 2;
}
  1. 两点同时向上跳
    由于直接向上不知道跳多长,考虑换一种思路,找最近公共祖先的下面最近的点。
    如果当前点 x x x y y y 向上跳 2 i 2^i 2i 步已经相等,说明跳上去的点已经是祖先了,进入下一次循环。
    否则, x x x y y y 都向上跳 2 i 2^i 2i 步。
    答案就为最终的 x x x y y y 的父亲。
for(int i = 25; i >= 0; -- i){//从大到小
	if(f[x][i] != f[y][i]){//不同,向上跳
		x = f[x][i];
		y = f[y][i];
	}
}
printf("%d", f[x][0]);

结合上文,我们就可以写出代码了。

//求每个点的深度
void dfs(int x, int y, int fa){
	dep[x] = y;
	f[x][0] = fa;//第 x 个点向上 1 步就是父亲
	for(auto i : v[x]){
		if(i == fa){
			continue;
		}
		dfs(i, y + 1, x);
	}
}
bool fl[200010];
int main(){
	输入 n
	for(int i = 1; i < n; ++ i){
		int x, y;
		scanf("%d %d", &x, &y);
		fl[y] = 1;//标记 y 有父亲
		//x 和 y 连边
		v[x].push_back(y);
		v[y].push_back(x);
	}
	//找 rt
	int rt;
	for(int i = 1; i <= n; ++ i){
		if(!fl[i]){//没有父亲
			rt = i;
			break;
		}
	}
	dfs(rt, 1, 0);
	预处理 f 数组,进行查询
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值