• 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

  • 相关阅读:
    常见排序算法总结(C语言版)
    “仿QQ局域网聊天软件”项目-常用编程技巧总结
    Java集合类之向量Vector
    Java集合类之LinkedList链表
    Java集合ArrayList的应用
    Java集合类之ArrayList
    Java二维数组
    二分查找
    快速排序法QuickSort
    插入排序InsertionSort
  • 原文地址:https://www.cnblogs.com/qwqq/p/10518677.html
Copyright © 2020-2023  润新知