填坑行动8-最近公共祖先LCA(树上倍增) 学习笔记

板子题

题目描述

如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

输入格式

第一行包含三个正整数 N , M , S N,M,S N,M,S分别表示树的结点个数、询问的个数和树根结点的序号。

接下来 N − 1 N−1 N1行每行包含两个正整数 x , y x, y x,y表示 x x x结点和 y y y结点之间有一条直接连接的边(数据保证可以构成树)。
接下来 M M M行每行包含两个正整数 a , b a,b a,b,表示询问 a a a结点和 b b b结点的最近公共祖先。

输出格式

输出包含 M M M行,每行包含一个正整数,依次为每一个询问的结果。

输入输出样例

输入 #1

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

输出 #1复制

4
4
1
4
4

说明/提示

对于 30 % 30\% 30%的数据, N ≤ 10 , M ≤ 10 N\leq10,M\leq 10 N10,M10
对于 70 % 70\% 70%的数据, N ≤ 10000 , M ≤ 10000 N\leq 10000,M\leq 10000 N10000,M10000
对于 100 % 100\% 100%的数据, N ≤ 500000 , M ≤ 500000 N\leq 500000,M\leq 500000 N500000M500000
样例说明:
该树结构如下:

第一次询问: 2 , 4 2,4 2,4的最近公共祖先,故为 4 4 4
第二次询问: 3 , 2 3,2 3,2的最近公共祖先,故为 4 4 4
第三次询问: 3 , 5 3,5 3,5的最近公共祖先,故为 1 1 1
第四次询问: 1 , 2 1,2 1,2的最近公共祖先,故为 4 4 4
第五次询问: 4 , 5 4,5 4,5的最近公共祖先,故为 4 4 4
故输出依次为 4 , 4 , 1 , 4 , 4 4,4,1,4,4 4,4,1,4,4

算法解析

首先大家都可以想到,这道题目可以使用 dfs ⁡ \operatorname{dfs} dfs来解决这个问题,但是,我们很容易得出这个算法并不能A掉这道题目, dfs ⁡ \operatorname{dfs} dfs的算法复杂度是 Θ ( N M ) \Theta\left(NM\right) Θ(NM),超时。
我们来分析一下 dfs ⁡ \operatorname{dfs} dfs的步骤:

  1. 将较低的节点提升,使两个节点在同一高度。
  2. 将两个节点提升,直到重合。

我们发现在上述过程中,都需要将两个节点向上提,最坏情况下提 N N N次,那么怎样才能吧节点提起更快呢?也就是一次不仅仅提一个。

倍增就可以做到。我们学过RMQ问题的同学都知道,RMQ使在一条链上进行倍增,而倍增版本的LCA也是相同的原理。令 f i , j f_{i,j} fi,j为节点 i i i向上提 2 j 2^j 2j个节点是多少,不难得出递推公式: f i , j = f f i , j − 1 , j − 1 \large f_{i,j}=f_{f_{i,j-1},j-1} fi,j=ffi,j1,j1
写在代码中就是:f[i][j]=f[ f[i][j-1 ][j-1]
解读:相当于节点 i i i向上提 2 j 2^j 2j次就相当于先提 2 j − 1 2^{j-1} 2j1次,在提 2 j − 1 2^{j-1} 2j1次即可。

预处理代码如下:

void dfs(int num,int pre){
    
    
	f[num][0]=pre;
	deep[num]=deep[pre]+1;
	for(int i=1;i<=t;i++)
	    f[num][i]=f[f[num][i-1]][i-1];
	for(int i=head[num];i;i=nex[i])
	    if(to[i]!=pre)
	        dfs(to[i],num);
}

得到了 f i , j f_{i,j} fi,j,我们就可以得出 LCA ⁡ ( a , b ) \operatorname{LCA}\left(a,b\right) LCA(a,b)了,具体做法和dfs一样,但是需要注意两点:

  1. 在提节点的时候要注意从大到小计算。
  2. 当两个节点位于同一高度时,要特判两个点是否重合。
  3. 注意在向上提两个点的时候,需要让两个点不相等,不然会提过头。

然后就是LCA的代码了:

int lca(int x,int y){
    
    
	if(deep[x]<deep[y]){
    
    
		x^=y;
		y^=x;
		x^=y;
	}//交换 
	if(deep[x]>deep[y]){
    
    
		for(i=t;i>=0;i--)
		    if(deep[f[x][i]]>=deep[y]&&f[x][i]!=0)
		        x=f[x][i];
		//if(deep[x]!=deep[y]) printf("*"); //翻车标记
	}
	if(x==y) return x;
	for(i=t;i>=0;i--)
	    if(f[x][i]!=f[y][i]&&f[x][i]!=0&&f[y][i]!=0)
	        x=f[x][i],y=f[y][i];
	//if(f[x][0]!=f[y][0]) printf("*"); //翻车标记 
	return f[x][0];
}

最后是完整的AC代码:

#include<cstdio>
#include<cmath>
#define maxn 500039
using namespace std;
int head[maxn],nex[maxn<<1],to[maxn<<1],k;
#define add(x,y) nex[++k]=head[x];\
head[x]=k;\
to[k]=y;
int u,v,f[maxn][20],deep[maxn];
int root,n,m,i,j,T,t;
void dfs(int num,int pre){
    
    
	f[num][0]=pre;
	deep[num]=deep[pre]+1;
	for(int i=1;i<=t;i++)
	    f[num][i]=f[f[num][i-1]][i-1];
	for(int i=head[num];i;i=nex[i])
	    if(to[i]!=pre)
	        dfs(to[i],num);
}
int lca(int x,int y){
    
    
	if(deep[x]<deep[y]){
    
    
		x^=y;
		y^=x;
		x^=y;
	}//交换 
	if(deep[x]>deep[y]){
    
    
		for(i=t;i>=0;i--)
		    if(deep[f[x][i]]>=deep[y]&&f[x][i]!=0)
		        x=f[x][i];
		//if(deep[x]!=deep[y]) printf("*"); //翻车标记
	}
	if(x==y) return x;
	for(i=t;i>=0;i--)
	    if(f[x][i]!=f[y][i]&&f[x][i]!=0&&f[y][i]!=0)
	        x=f[x][i],y=f[y][i];
	//if(f[x][0]!=f[y][0]) printf("*"); //翻车标记 
	return f[x][0];
}
int main(){
    
    
    scanf("%d%d%d",&n,&T,&root);
    for(i=1;i<n;i++){
    
    
    	scanf("%d%d",&u,&v);
    	add(u,v);
    	add(v,u);
	}
	deep[0]=-1;
	t=(int)log2(n)+1;
	dfs(root,0);
	int x=0,y=1;
	while(T--){
    
    
		scanf("%d%d",&u,&v);
		printf("%d\n",lca(u,v));
	}
	return 0;
}

都0202年了应该没人用邻接矩阵的吧。

关于LCA的拓展

LCA不仅仅能处理最近公共祖先,而且还可以处理两个点到最短公共祖先的距离、最短路、最大最小点权、最大最小边……这里就不一一叙述了。

猜你喜欢

转载自blog.csdn.net/jiangtaizhe/article/details/107440504