LCA(最近公共祖先)

什么是最近公共祖先?

来自百度百科的定义:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。


首先让我们给出一个题目【NKOJ2447-最近公共祖先】


那么我们应该怎样去求最近公共祖先?

我们发现,如果使用暴力枚举第一个点所有的祖先,再看从深度最大的第二个点的祖先去一一验证是否有交集的话,时间复杂度是O(N)的。所以肯定会T。
ORZ。
(不然我们为什么要用LCA)


需要申明的数组

Dep[v]:用于记录节点v的深度;
Fa[v][k]:用于记录节点v向上第(2^k)个祖先的编号;
Last[x],End[i],Next[i]:存图;


1.Fa[v][k]中之所以存的是(2^k),是因为在接下来的操作中会使用到位运算;
2.在接下来的操作之中,我们还需要有一个必要的DFS来递归建树;
3.除了Fa[v][k]数组需要预处理以外,其他数组的初值均可赋值为0;


LCA的核心代码讲解

预处理

    Fa[v][k] = Fa[Fa[v][k - 1]][k - 1];

从v号点向上走P层

    void Go_Up(int v, int p){
    for(int i = 1; i <= s; i ++)//s为倍增上限,注意此处是i < s
        if(p & (1 << i) ) v = Fa[v][i];
    }

在x与y在同一层找二者的公共祖先

    if(x == y)return x;//假设之前Dep[x] < Dep[y],那么此情况则相当于之前x是y的子孙
    int s = ceil(log2(Dep[x]));//向上倍增的上限
    for(int i = s; i  >= 0; i --){
        if(Fa[x][i] != Fa[y][i]){
            x = Fa[x][i];
            y = Fa[x][i];
        }
    }
    return Fa[x][0];
在这里,有几个思考的问题:

1.为什么i层的循环是从s到1?
2.为什么祖先相同的时候不向上走,祖先不同的时候反而向上走?
3.最后的结果是什么?

对于第1,2个问题,我们来作这样的一个假设:
假设x是u和v的最近公共祖先,那么会有这样的两条结论
* x往上到根的路径上的所有点都是u和v的公共祖先;
所以当fa[u][i]==fa[v][i]条件成立时,fa[u][i]不一定是它们的最近公共祖先。
* 若fa[u][i]!=fa[v][i],说明点fa[u][i]和点fa[v][i]一定是x下方的点
由此,我们不难看出,1,2这两种措施都是为了防止“跳过头”,以免所求仅满足了“公共祖先”而未满足“最近”

所以说第3个问题的答案也就出来了。x,y停下来的位置再向上数一层既是所求的最近公共祖先


到这里,我们就讲解完了LAC的核心代码,接下来,我将给出LCA代码的模版(也就是在一开始我给出的例题的完整代码,我将会在其中以注释的方式进一步阐述说明)
建议先看LCA函数,再看主函数,最后看DFS函数


LCA代码模版

#include <stdio.h>
#include <bits/stdc++.h>
using namespace std;
int n, m, x, y;
int End[10005], Next[10005], Last[10005], Dep[10005],Fa[10005][20];

void DFS(int x){
    Dep[x] = Dep[Fa[x][0]] + 1;
     //由于在DFS中,我们是由根节点从上往下讨论的,所以我们可以保证x的father已经被算了出来
    int s = ceil(log2(Dep[x]));
    //计算倍增上限,ceil向上取整

    for(int i = 1; i <= s; i ++)
        Fa[x][i] = Fa[Fa[x][i - 1]][i - 1];
        ////倍增计算祖先 【核心代码1】
        
    for(int i=Last[x];i;i=Next[i])
    	if(End[i]!=Fa[x][0])Fa[End[i]][0]=x,DFS(End[i]);
    	
}
int LCA(int x, int y){
	int i, k, s;
	s = ceil(log2(n));//计算倍增上限 
	if(Dep[x] < Dep[y])swap(x, y);//保证x在y的下方,交换无影响 
	k = Dep[x] - Dep[y];
	
	for(int i = 0 ; i <= s; i ++)
		if(k & (1 << i))x = Fa[x][i];//GO_UP 【核心代码2】
	
	if(x == y)return x;//如果x == y,则说明x之前是y的子孙,那么x,y的最近公共祖先就是y(也就是现在的x)
	
	s = ceil(log2(Dep[x])); 
	
	for(int i = s; i >= 0; i --)//【核心代码3】
		if(Fa[x][i] != Fa[y][i]){
			x = Fa[x][i];
			y = Fa[y][i];
		}
		
	return Fa[x][0];//此时Fa[x][0](x节点向上数一层)就是x,y的最近公共祖先
}

int main(){
	scanf("%d", &n);
	for(int i = 1; i < n ; i ++){
		scanf("%d%d", &x, &y);
		Fa[y][0] = x;//记下y的father
		End[i] = y;
		Next[i] = Last[x];
		Last[x] = i;
	}

	for(int i = 1; i <= n; i ++){
		if(Fa[i][0] == 0){// 2^0 == 1,所以Fa[i][0]就记的是该节点的父亲
			DFS(i);
			break;
		}
	}
        //由于这是一棵树,所以说这一步代码的目的就在于找出没有父亲的孤儿节点——即根节点root
        //DFS的目的即初始化Fa[v][k]数组

	scanf("%d", &m);
	for(int i = 1; i <= m; i ++){
		scanf("%d%d", &x, &y);
		printf("%d
", LCA(x, y));
	}
	return 0;
}

LCA大概就是这样orz,由于我太菜了所以说可能会有表述不清楚的地方,所以如果有没有理解到的地方欢迎骚扰什么的orz

原文地址:https://www.cnblogs.com/qwqq/p/10518677.html