LCA问题——倍增算法,Tarjan算法讲解

LCA问题

前言:助力信息奥赛!

1.什么是LCA

LCA(Least Common Ancestors)最近公共祖先问题。指在有根树中,找出某两个结点u和v最近的公共祖先。图示👇在这里插入图片描述
当然这和并查集有点像,只不过并查集找的是root,你LCA可能找到的不是root。
当然对于暴力来说还是很容易想到的,就是俩点一个一个向上跳呗,但前提是它俩得同一深度才行啊,要不万一深度比较小的那个点和你深度深的那个点一块蹦,人家蹦没影了,你还在这打转转…

2.倍增算法

在一般情况下,求解 LCA 问题套用暴力方法即可,因为一棵 n 个节点的树,平摊树高只有 log(n) 。但在竞赛中,一般会构造一些 乱七八糟 树,使得暴力的方法会超时。
现在介绍一种更好的算法:倍增!
也就是说:在每一个节点上,记录 log(n) 个节点信息,分别对应向上走 1 步, 2 步, 4 步, 8 步… 2k 步 所到达的节点。这样我们只需要 log(n) 步就可以走到根节点。然后仍然套用之前提到的暴力方法向上走即可。
以下内容大佬们自动跳过👇
十进制的数能拆,咋拆呢?举个例子

7 的二进制是 111 那么你就可以写成  2º + 2² + 2³ = 7(打这几个次数有点难)
所以倍增的基本思想就差不多能明白了(自信)。

那么现在就是怎么记录这些记录的问题了👇
初始我们只知道向上走 1 步的信息。
然后根据走 1 步的信息,推出走 2 的信息。
再根据走 2 步的信息,推出走 4 的信息。

以此类推。

具体来说,我们用数组 f[u][j] 记录节点 u 向上走 2^j 步走到的点,显然有 f[u][0] = u 的父亲节点,且 f[u][i] = f[f[u][i−1]][i−1] (好好理解理解)。

因此 f 数组可以根据递推式得到。

for (int i = 1; i <= 20; i++)
    for (int u = 1; u <= n; u++)
        f[u][i] = f[f[u][i - 1]][i - 1];

解决让一个节点向上跳 k 步:

问——为啥解决这个k呢?
答——让你少跳几步!
问——我想一步登天。
答——一步登天还用你求干嘛?

解决方案👇
先把k拆了(和我前面说的一样)在这里插入图片描述
那么可以让 k 先通过 f[k][a1] 跳 2a1 步到 k′ ,再通过 f[k′][a2] 跳 2a2 步到 k′′ … 依次类推,只需要 t 次就能向上跳 k 步了,且 t 约等于 log2k 。
在求 x,y 两个节点的 LCA 时,如果 x,y 深度不同,则先让深度较大的那个节点向上跳到另一个节点所在的深度。记 dep[u] 表示节点 u 的深度,假设 dep[x]>dep[y] ,那么先让 x 倍增的向上跳 dep[x]−dep[y] 步。

int tmp = dep[x] - dep[y];
for (int i = 20; i >= 0; i--)
    if (tmp & (1 << i))
        x = f[x][i];

当两个节点深度相同时,就同时倍增的向上跳,但是又不能让他们跳到 LCA 之上。具体来说,我们从大到小枚举 j ,判断 x,y 同时向上走 2^j步是否会相遇,如果不会,则向上跳 2^j步。重复这个过程,此时 x,y 就会跳到其 LCA 的儿子处(一定注意是儿子,不是LCA!),此时再进一步就是 LCA 。

for (int i = 20; i >= 0; i--)
    if (f[x][i] != f[y][i])
        x = f[x][i], y = f[y][i];
if (x != y)
    x = f[x][0], y = f[y][0];
//此时变量x,y就是Lca

3.Tarjan算法求解LCA

这是一种离线算法
所谓离线算法,就是说它不会在查询两个节点 LCA 这个事件发生时就及时的执行并给出答案,而是等到所有查询都给出后统一进行处理和求解。
为啥介绍这种算法呢,因为时间效率非常高,它的复杂度已经降到了 O((n+q)× 并查集的复杂度 ),而同时使用路径压缩和按秩合并的并查集复杂度为 O(Alpha(n)) ,因此 Tarjan 算法的时间复杂度为 O((n+q)Alpha(n)) 。(好处挺多,学吧)

1.在对一棵树进行 DFS 的过程中,对于一对点 u 和 v ,我们肯定是会先遍历到其中一个,后遍历到另外一个。

2.不妨假设先遍历到 u ,后遍历到 v 。

3.现在你看一下u和v的关系 ,很显然 DFS 算法从 u 开始回溯到其祖先节点,并递归遍历其祖先节点的其它子树,当遍历到某个祖先节点 p 时, v 恰好也是 p 的子树中的节点,那么在递归遍历 p 的子树时就会遍历到 v 。
也就是说这个p点是u的众多祖先中,深度是最深的并且同时是u和v的祖先,那么这个p点就是我们要求的最近公共祖先呀!

说一下步骤👇

  1. 维护一个并查集,在起始时每个点分别各自属于同一个并查集。
  2. 任选一节点Root然后开始进行深搜
  3. DFS到一个u点后,遍历其所有子节点v,且将v标记“已访问”,递归DFS
  4. 开始合并v节点和u节点的并查集,且将find(v) 作为
    并查集的子节点连在finf(u)下面
  5. 枚举与u节点相关的询问(u,x),如果x已经被访问过,那么Lca(u,x) = find(x)。
    伪代码理解理解👇
tarjan(u){
	for(u -> v){//1.访问所有u的子节点 
		vis[v] = 1;
		tarjan(v);//continue 
		merge(u,v);//把v合并到u上 ,同时标记一下 
	}
	for(u,x){
		if(vis[x])
			ans[(u,x)] = find(x);
			//如果x被访问过了,那么u,x的最近公共祖先就是find(x) 
	}
} 

4.模板

来一个倍增的模板帮助大家理解👇

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
typedef long long ll;
int head[MAXN], deep[MAXN], fa[MAXN][16];
ll dis[MAXN];
struct data{
	int z, val, nexty;
};
data edge[MAXN << 1];
int cnt;
void add(int u,int v,int w){
	cnt++;
	edge[cnt].z = v;
	edge[cnt].val = w;
	edge[cnt].nexty = head[u];
	head[u] = cnt;
}
int dfs(int x, int par){
	for(int i = 1; i <= 16; i++){
		if(deep[x] < (1 << i)) break;
	    fa[x][i] = fa[fa[x][i - 1]][i - 1];
	}
	for(int i = head[x]; i; i = edge[i].nexty){
		if(edge[i].z == par) continue;
		deep[edge[i].z] = deep[x] + 1;
		dis[edge[i].z] = dis[x] + edge[i].val;
		fa[edge[i].z][0] = x;
		dfs(edge[i].z, x);  
	}
}
int lca(int x, int y){
	if(deep[x] < deep[y]){
		swap(x, y);
	}
	ll d = deep[x] - deep[y];
	for(int i = 0; i <= 16; i++){
		if((1 << i) & d)
			x = fa[x][i];
	}
	if(x == y){
		return x;
	}
	for(int i = 16; i >= 0; i++){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}
int main(){
	int n, q;
	cin >> n >> q;
	for(int i = 1; i < n; i++){
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
		add(v, u, w);
	}
	dfs(1, 0);
	for(int i = 0; i < q; i++){
		int x, y;
		cin >> x >> y;
		cout << dis[x] + dis[y] - dis[lca(x, y)] * 2 << endl;
	}
	return 0;
}

THE END~
女朋友觉得我敲键盘之后手会磨出茧,于是给我买了个缠手的“防磨胶带”(打字不便 )还是挺感动滴

最近看到个新闻挺好玩的👇
在这里插入图片描述
LCA战斗机~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值