最近公共祖先(LCA)

代码针对洛谷P3379

1.向上标记法

最朴素的求最近公共祖先的方法,两个子结点同时向上一个一个的跳,标记已经跳过的点,如果有一个点跳到了已经标记过的点,那么就说明这个点就是最近公共祖先。可以自己简单的推一下,查询的时间复杂度为 $ O(n) $ 。

2.倍增

总思路:用类似于二进制拆分的思想。

首先我们先理清楚二进制数的特点,对于一个数$2^n$,我们可以用$2^0,2^1…2^n-1$,来表示出任何一个小于$2^n$的数。

举例:

$8=1000\,=2^3$

$7=0111\,=2^2+2^1+2^0$

$6=0110\,=2^2+2^1$

$5=0101\,=2^2+2^0$

$4=0100\,=2^2$

$3=0011\,=2^1+2^0$

$2=0010\,=2^1$

$1=0001\,=2^0$

下面来介绍一下算法的思路,来解释一下为什么与二进制数的拆分有关系。

算法思路:

当x与y在不同深度时,最近公共祖先肯定不会是深度大的那个结点,所以说,我们可以将x与y调整到同一深度,将深度大的点不断往上跳,直到两个点深度相同。这与求两个点的最近公共祖先是等效的。

那么要怎么样让结点小的点往上跳呢,首先,我们要进行一遍处理,预处理出每个节点的第$2^k$的父结点。这样子在往上跳的时候,就可以直接跳到2的k次方。

那么我们首先将深度大的点不断向上跳,直到两个点深度相同。(具体可以看代码,这里就要用到二进制拆分了。)

这里如果要推的话有个推理,当求两个点(如x和y)的最近公共祖先时,如果x或y不是最近公共祖先那么求x和y的最近公共祖先等效于求x的父结点和y的父结点的最近公共祖先。

因此我们可以推出,当x与y的$2^k$祖先不是最近公共祖先时,我们可以直接将问题改为求x的$2^k$的父结点和y的$2^k$的父结点的最近公共祖先。

那么当x和y的$2^k$的祖先相同时,我们就不往上跳,当x与y的$2^k$的祖先不同时,我们就向上跳$2^k$,最后可以保证跳完后x的祖先或y的祖先即为x和y的最近公共祖先。

而上面讲述二进制拆分就是为了讲述如何保证不跳少了。

比如说如果x和y的最近公共祖先是x的第7个父结点时,刚开始从$2^k$,一支循环到$2^3$,发现x的第8个父结点与y的第8个父结点是相同的,所以不往上跳,而剩下的$2^2$发现父结点不同,向上跳4,$2^1$,发现父结点不同,向上跳2,最后发现父结点相同了不在往上跳。那么此时x以及y的父结点就是要求的最近公共祖先。

代码:

#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#define NN N<<1
using namespace std;
const int N=5e5+10;
int n,m,s,t,a,b,d[N],f[N][200],e[NN],ne[NN],h[N],cnt;
queue<int> q;
void add(int a,int b){
	e[++cnt]=b;
	ne[cnt]=h[a];
	h[a]=cnt;
}

void bfs(int s){
	q.push(s);d[s]=1;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=h[x];i;i=ne[i]){
			int y=e[i];
			if(d[y])continue;
			d[y]=d[x]+1;
			f[y][0]=x;
			for(int i=1;i<=t;i++){
				f[y][i]=f[f[y][i-1]][i-1];
			}
			q.push(y);
		}
	}
}

int lca(int x,int y){
	if(d[x]>d[y]) swap(x,y);
	for(int i=t;i>=0;i--){
		if(d[f[y][i]]>=d[x]) y=f[y][i];
	}
	if(x==y) return x;
	for(int i=t;i>=0;i--){
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<n;i++){
		scanf("%d%d",&a,&b);
		add(a,b);
		add(b,a);
	}
	t=(int)(log(n)/log(2))+1;
	bfs(s);
	
	for(int i=1;i<=m;i++){
		scanf("%d%d",&a,&b);
		printf("%d\n",lca(a,b));
	}
}

3.树链剖分

树链剖分有很多种,这里讲的是重链剖分,简单的来说是对于每个结点来说,将他的子儿子中子树的节点数最多的子儿子作为重儿子(等图片上来后更清楚)。

重链剖分:用子节点个数来确定重儿子和轻儿子,且将连续的重儿子连成一条链,使得树被分为多条链。

实现:

两次dfs,第一次,找出重儿子,深度,父节点。第二次dfs,确定点的dfn序和rnk序,以及每个点的重顶点。

查询:

若在同一条重链上,则深度小的点为最近公共祖先。若不在,则将重顶点的深度大的点通过跳到链顶的方式,直到两个点到同一条重链上。

证明:

这里是我的不严谨证明:

若两个节点不在同一条重链上,则最近公共祖先(下面以lca代替)不可能在链顶深度大的重链中,此时,求x与y的lca与x与top[y]的lca相同,同理x与f[top[y]]的lca相同,以此直到在同一条重链中。

代码:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdio.h>
#define NN N<<1
using namespace std;
const int N=5e5+10;

int n,m,s,h[N],e[NN],ne[NN],son[N],top[N],siz[N],f[N],dep[N];
int a,b,cnt;

void add(int a,int b){
	e[++cnt]=b;
	ne[cnt]=h[a];
	h[a]=cnt;
}
void dfs1(int x){
	son[x]=-1;
	siz[x]=1;
	for(int i=h[x];i;i=ne[i]){
		int ver=e[i];
		if(ver!=f[x]){
			if(!dep[ver]){
				dep[ver]=dep[x]+1;
				f[ver]=x;
				dfs1(ver);
				siz[x]+=siz[ver];
				if(siz[son[x]]<siz[ver]) son[x]=ver;
			}
		}
	}
}
void dfs2(int x,int t){
	top[x]=t;
	if(son[x]==-1) return;
	dfs2(son[x],t);
	for(int i=h[x];i;i=ne[i]){
		int ver=e[i];
		if(ver!=son[x]&&ver!=f[x])dfs2(ver,ver);
	}
}
int lca(int a,int b){
	while(top[a]!=top[b]){
		if(dep[top[a]]>dep[top[b]]) a=f[top[a]];
		else b=f[top[b]];
	}
	return dep[a]<dep[b]?a:b;
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<n;i++){
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}
	dfs1(s);dfs2(s,s);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&a,&b);
		printf("%d\n",lca(a,b));
	}
}

4.Tarjan

还不会

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇