【算法竞赛刷题模板10】基于倍增算法求LCA

0.总结

  • Get to the points first. The article comes from LawsonAbs!
  • 二进制拆分
  • LCA的树上倍增算法实现
  • 当我们已经得到祖先节点,如何获取某两个节点的祖先节点?
  • 博客来源: LawsonAbs@CSDN

1.问题

在谈LCA之前,说一下什么是二进制拆分

1.1 二进制拆分

我们都知道,计算机中的数都是用0/1来表示的,也就是说任何一个正整数都可以用二进制来表示出来,但是小数却是不行的。用二进制来表示任何一个正整数的思想就叫做二进制拆分

1.2 LCA问题

  • 给定一棵树【说明无环】的两个节点a,b
  • 节点a到根节点的路径 和 节点b到根节点的路径上的第一个交点【深度最大的一个点】就是LCA

2.分析

  • 使用二进制拆分思想倍增的求出LCA
  • 对于普通的输入,先指定根节点
  • 预处理部分:
    (1)用bfs得到每个节点的深度hei[i]
    (2)定义f[i][j]表示节点i往根节点方向走2j步到达的节点编号。如果往上走的步超过了根节点的高度,那么就设其值为0。问题关键是怎么得到这个数组的值?
    可以推导节点间满足公式:f[i][j] = f[[f[i][j-1]][j-1],故在 bfs 的时候,用一个for循环进行相应的操作进行赋值即可。
    (3)使用 lca(int x,int y) 求出LCA 【我通常是在这一步出现问题】。lca()函数主要是三步骤:
    step1.迭代更新,直到两个节点的深度相同
    step2. 深度相同时,可能存在两种情况:
    a. 两个节点为同一节点,此时该节点就是LCA
    b. 两个节点只是在同一层,但LCA还在高处,走多少步还是不知道的,所以还是需要从大往小走 ,该如何从大往小走呢?

需要注意的点:

  • 事先求出树的最大高度k = log2(n),此时树是一条链。
  • for循环中i的初值就是这个k。因为最大就是到2k步。接着一次次的缩小即可。
  • 在for循环中不停地更新二者的值时,防止将值取到0【也就是temp不能为0】

3.代码

3.1 代码

#include<cstdio>
#include<iostream>
#include<cmath>
#include<queue> 
using namespace std;
const int maxN = 500005;//最大结点数 
const int maxM = 1000010; //因为是无向边 ,所以要*2 
const int maxD = 25;//最深 

typedef struct {
	int next,to;
}Edge ;

typedef struct {
	int id,de;
}Node;

queue<Node> que;
Edge edge[maxM];//边长 
int f[maxN][maxD];
//f[i][j] 表示节点i向上走2^j步所能到达的节点的序号 ;
//如果f[i][j]=0,则表示 从i节点向上走2^j步,没有父节点 
int hei[maxN],vis[maxN],head[maxN];//深搜知道各个节点的深度;该顶点是否访问过;每个顶点的头指针 
int eN;//边数
int n,m,s,k;//k = log2(n) 

void add(int a,int b){
	edge[eN].next = head[a];
	edge[eN].to = b;
	head[a] = eN;
	eN++; 
} 

//预处理f[][]
void bfs(){
	Node temp;
	que.push((Node){s,0});
	int topId,deep;//队首;队尾
	int eTo;//相邻的点 
	while(!que.empty()){//队列非空时		 
		topId = que.front().id;//取队头拿id 
		deep = que.front().de;//取队头拿深度 
		vis[topId] = 1;//已访问 
		hei[topId] = deep;//记录深度,后面用到 
		que.pop(); 
		//计算f[][]
		if(deep > 1){
			int k = log2(deep); //最多走2^k就到根节点了 
			for(int i = 1;i <=k; i++){ //利用递推求得关系式 
				f[topId][i] = f[ f[topId][i-1]][i-1];
			} 
		}
					
		for(int i = head[topId]; i!=-1; i=edge[i].next){					
			eTo = edge[i].to;	
			if(vis[eTo]){//如果该顶点已经访问过了,则不能再放进去了 
				continue;
			} 
			que.push((Node){eTo,deep+1});//放进队列
			f[eTo][0] = topId;//这是很重要的一步 ,不能疏漏了 
		}
	}	
}


void lca(int x,int y){
	//step1.迭代更新,直到将更深的y变得与x的深度相同 
	for(int i = k;i >= 0 && hei[y] > hei[x];i--)
	{
		int temp = f[y][i];//向上走2^i步到的节点编号为temp
		//如果走过头了,temp就为0了,任何hei[i]都大于hei[0],所以要避免这种情况 
		if(temp&&hei[temp]>=hei[x]){
			y = temp;
		}
	}
	
	//step2. 深度相同时,情况1:二者已经相同了,此时就是LCA 
	if(x == y){//因为x较浅 
		printf("%d\n",x);
	}
	//step3.深度相同时:情况2:可能只是同一层,但LCA还在高处,走多少步还是不知道的,
	//所以还是需要从大往小走 
	else{		
		for(int i = k;i>=0;i--){//依然是从大往小走 
			if(f[x][i]!= f[y][i]){//保证二者不相会 => 因为要找出最小的lca 
				x = f[x][i];
				y = f[y][i];
			}
		} 	
		//最后还得走一步 
		printf("%d\n",f[x][0]) ;
	}
} 

int main(){	
	scanf("%d%d%d",&n,&m,&s);
	k = log2(n);
	int a,b;//有边相连 
	fill(head,head+maxN,-1);//初始化为-1 
	fill(f[0],f[0]+maxN*maxD,0);//初始为0 
	for(int i = 0;i<n-1;i++ ){
		scanf("%d%d",&a,&b);		
		add(a,b);
		add(b,a); 
	}
			
	bfs(); 

//验证输出f[][]函数是否搞对了
//	for(int i = 1;i<=n;i++){
//		cout <<i<<":  ";
//		for(int j = 0;j<log2(n);j++){
//			cout <<f[i][j] <<" ";
//		}cout <<"\n";
//	}

	for(int i = 0;i<m;i++){
		scanf("%d%d",&a,&b);		
		if(hei[a] > hei[b]) //比较高度, 
			lca(b,a);//a更深 
		else
			lca(a,b);//b更深 
	}
}
3.2 测试用例:
9 5 1
1 2
1 3
2 4
2 5
5 9
3 6
3 7
6 8
1 1
1 2
2 8
6 7
8 7

3 3 1
1 2
2 3
1 2
1 3
2 3
4.相关例题
  • P3379 【模板LCA】
    有很多题【可以说只要涉及到树上两点间的距离这种问题】都是基于LCA来做,所以熟练这个 树上倍增求LCA 就相当重要了。相关的例题可以查看题单——基础树上问题。私以为,LCA是进阶的基础内容,是必须啃下的!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

说文科技

看书人不妨赏个酒钱?

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

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

打赏作者

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

抵扣说明:

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

余额充值