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是进阶的基础内容,是必须啃下的!