[LCA]最近公共祖先(倍增+树剖)

倍增求 LCA

概念引入

在这里插入图片描述

祖先

祖先其实很好理解,一个节点的 **父节点 以及 父节点的父节点 以及 父节点的父节点的父……**都是这个节点的祖先

比如说上面的 d d d 节点, b b b 节点和 a a a 节点都是它的祖先

k k k 级祖先

称节点 𝑥 的父节点为 𝑥 的 1 级祖先。节点 𝑥 父节点的 𝑘 级祖先称为节点 𝑥 的 𝑘 + 1 级祖先。

比如,节点 a a a 就是节点 d d d 的二级祖先

引例

假设节点 i i i k k k 级祖先是 j j j j j j k 1 k1 k1 级祖先为 x x x ,那么 x x x i i i 的几级祖先?

显然是 ( k + k 1 ) (k + k1) k+k1 级祖先

深度

d e p ( x ) dep(x) dep(x) 为节点 𝑥 的深度。
若 𝑥 为根结点,则 d e p ( x ) = 1 dep(x) = 1 dep(x)=1
否则 d e p ( x ) = d e p ( f ) + 1 dep(x) = dep(f)+1 dep(x)=dep(f)+1,其中 f f f x x x 的父节点。

基本思想

考虑树上深度相同的节点对 (𝑥, 𝑦),设其 𝐿𝐶𝐴 为节点 𝐿。
Δ = d e p ( x ) − d e p ( L ) \Delta = dep(x) - dep(L) Δ=dep(x)dep(L)
显然, Δ > 0 \Delta > 0 Δ>0,为节点对 ( x , y ) (x,y) (x,y) L L L 的深度差,即节点 ( x , y ) (x,y) (x,y) 想要抵达节点 L L L,需要向上跳跃的距离。

我们只需要求出节点 x x x y y y Δ \Delta Δ 级祖先,就求出了节点对 ( x , y ) (x,y) (x,y) 的最近公共祖先,但 Δ \Delta Δ 具体的值并不明确,采用尝试的办法。
不妨记 F ( x , k ) ( k ≥ 0 ) F(x,k) (k \geq 0) F(x,k)(k0)𝐹为节点 𝑥 的 2 k 2^k 2k 级祖先。
由倍增思想,从高位 ( l o g 2 n ) (log_2 n) (log2n)向低位( 0 0 0)依次枚举 i i i
F ( x , i ) = F ( y , i ) F(x,i) = F(y,i) F(x,i)=F(y,i),说明 x x x 2 i 2^i 2i 级祖先在节点 L L L 到根节点的路径上,不作处理。
否则, x ← F ( x , i ) , y ← F ( y , i ) x \leftarrow F(x,i) , y \leftarrow F(y,i) xF(x,i),yF(y,i)
i = 0 i=0 i=0 枚举完毕后, x x x, y y y 节点的父节点即为其最近公共祖先。

代码实现

F ( x , k ) F(x,k) F(x,k) 则可以通过递推在预处理中求出。
F ( x , 0 ) = f F(x,0) = f F(x,0)=f
F ( x , k ) = F ( F ( x , k − 1 ) , k − 1 ) ) F(x,k) = F ( F(x,k-1),k-1)) F(x,k)=F(F(x,k1),k1))
这可以通过一次树上遍历完成。
d e p ( x ) ≠ d e p ( y ) dep(x) \not= dep(y) dep(x)=dep(y),不妨设 d e p ( x ) > d e p ( y ) dep(x) > dep(y) dep(x)>dep(y),先通过一次倍增,将节点 x x x 向上跳跃至与节点 y y y 深度相同。

Code

#include <bits/stdc++.h>
#define ll long long
const int INF = 0x3f3f3f3f;
const int N = 5e5+10; 
using namespace std;
int n, m , s;
vector <int> e[N];
int fa[N][25],dep[N];
void get_father(int pos,int f){
	fa[pos][0] = f;
	dep[pos] = dep[f] + 1;
	for(int i = 1; i <= 20; i++){
		fa[pos][i] = fa[fa[pos][i-1]][i-1];
	}
	for(auto j : e[pos]){
		if(j == f) continue;
		get_father(j,pos);
	}
}
int LCA(int x, int y){
	if(dep[x] < dep[y]){
		swap(x,y);
	}
	for(int i = 20; i >= 0; i--){
		if(dep[fa[x][i]] >= dep[y]){
			x = fa[x][i];
		}
	}
	if(x == y) return x;
	for(int i = 20; i >= 0; i--){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}
int main(){
	cin >> n >> m >> s;
	for(int i = 1;i < n; i++){
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}	
	get_father(s,0);
	while(m--){
		int x, y;
		cin >> x >> y;
		cout << LCA(x,y) << endl;
	}
	return 0;
}

树链剖分求 LCA

概念引入

这里树链剖分也叫重链剖分

子树大小

以节点 x x x 为根的子树所含节点数称为该子树的大小,记为 S ( x ) S(x) S(x)

轻重儿子

V ( x ) V(x) V(x) 为节点 x x x 的子节点集合。对于 u ∈ V ( x ) u \in V(x) uV(x),满足 ∀𝑣 ∈ 𝑉 𝑥 , 𝑆 𝑣 ≤ 𝑆(𝑢),则 u u u 称为节点 x x x 的重儿子,其余子节点称为 x x x 的轻儿子。特别地,若 V ( x ) = ϕ V(x) = \phi V(x)=ϕ,节点 x x x 不存在重儿子。(一个节点可能有多个重儿子,任取其一)

重链

这里不介绍有关 d f s dfs dfs 序,因为用不到
重儿子与其父节点的连边叫做一条重链

轻链

当然不是重链就是轻链了

这里的重链和轻链都是满足可加性

在这里插入图片描述
例如这里标小红点的是轻儿子,粗线是重链

跳链头

在重链剖分中有一种很重要的操作是跳链头

重链相当于高速公路,每次可以跳到深度最小的节点的父节点(不然的话就是一条重链上出不去了)
轻链相当于普通公路,一次只能跳一步

重链剖分

重链剖分由两次 d f s dfs dfs 完成

第一次 d f s dfs dfs :计算每一棵子树的 s i z e , d e p t h , H s o n , F a t h e r size,depth,Hson,Father size,depth,Hson,Father
第二次 d f s dfs dfs :按照先重儿子,后其他儿子的顺序,对树上的结点进行重新编号(仅用于求 LCA 不需要),并维护每一个结点所处的重链的链头 原来的编号 t o p ( x ) top(x) top(x)

Code

int top[N],hson[N],siz[N],dep[N],f[N];
void dfs1(int x, int fa){
	hson[x] = 0;
	f[x] = fa;
	siz[x] = 1;
	dep[x] = dep[fa] + 1;
	for(auto to : e[x]){
		if(to == fa) continue;
		dfs1(to,x);
		siz[x] += siz[to];
		if(siz[hson[x]] < siz[to]) hson[x] = to;
	}
	
}
void dfs2(int x, int tp){
	top[x] = tp;
	if(!hson[x]) return;
	dfs2(hson[x],tp);
	for(auto to : e[x]){
		if(to == f[x] || to == hson[x]) continue;
		dfs2(to,to);
	}
}

求 LCA

使用重链剖分求 L C A LCA LCA ,就是不断交替跳链头的过程,直到两个结点游标处于同一条重链上。
交替:优先跳 链头 深度深的游标。

然后返回深度浅的节点就行

Code

int LCA(int x, int y){
	while(top[x] != top[y]){
		if(dep[top[x]] < dep[top[y]]) swap(x,y);
		x = f[top[x]];
	}
	if(dep[x] < dep[y]) return x;
	return y;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bamboo_Day

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值