最近公共祖先(LCA)笔记

一、LCA 是什么

最近公共祖先是指对于有根树T的两个结点 $u$、$v$,满足一个结点 $x$ 既是 $u$ 的祖先,也是 $v$ 的祖先,且 $x$ 的深度尽可能大。在这里,一个节点也可以是它自己的祖先。

如下图所示,当$u=4$,$v=7$时:

$u$ 的祖先有:$4$、$\mathbf{2}$、$\mathbf{1}$。

$v$ 的祖先有:$7$、$5$、$\mathbf{2}$、$\mathbf{1}$。

它们的公共祖先分别有 $2$、$1$,其中 $2$ 的深度最大,因此, $2$ 是 $4$ 和 $7$ 的最近公共祖先 (虽然说肉眼也能看出来吧)

二、LCA 如何实现

1. 枚举法:

把 $u$ 的所有祖先标记起来,再把 $v$ 的所有祖先标记起来。此方法最为简单,也最为无脑,查询一次的时间复杂度高达 $O(n)$,大部分题目都无法通过。

2. 倍增法

(1). 思路

先把要查询的 $u$ 与 $v$ 拉到同一层,然后按照 $2^i$、$2^{i-1}$、$2^{i-2}$ ... $2^1$、$2^0$ 的顺序枚举步数,使其跳跃到达的深度越大越好,最后到达其最近公共祖先的下一层结束,返回结果。

(2). 做法

①. 建图

我们需要采用一种容器来存储这张树,在这里选择 链式前向星 的原因,是其优秀的 时间复杂度空间复杂度,能够容纳下大量的数据

struct EDGE{
    int v,next;//v表示编号为i的边的终点,next表示以第i条边的起点为起点的下一条边的编号。
}edge[N];
int head[N],idx=1;//head[i]表示以i为起点的最后一条加入的边的编号。
void add_edge(int u,int v){
    edge[idx].v=v;//将终点设置为终点。
    edge[idx].next=head[u];//第idx条边的下一条边的编号即为以u为起点的加入的最后一条边的编号。
    head[u]=idx;//以u为起点的最后一条加入的边更新。
    idx++;//编号跳到下一条边。
}

②. 确定深度与每步跳到的点位

确定了容器之后,我们就要确定每一个点的 深度 与它跳 $2^i$ 步能到达的点的 编号

首先,我们 确定一个根节点 作为整棵树的根,然后用深搜算法遍历这颗树的每一个节点,一路一边将他们的层数设置为他们的祖先的上一层,一边用状态转移方程将他们跳 $2^i$ 步能到达的点传下去。

若用 $dep_i$ 表示编号为 $i$ 的节点的深度,用 $u$ 来表示当前节点的编号,用 $fa$ 来表示其父亲节点的编号,那么就可以很容易的得到:$$dep_u=dep_{fa}+1$$

若用 $f_{i,j}$ 来表示编号为 $i$ 的节点跳 $2^j$ 步能到达的节点的编号,那么就相当于从 $i$ 出发先跳 $2^{j-1}$ 步再跳 $2^{j-1}$ 步,所以状态转移方程即为:$$f_{i,j}=f_{f_{i,j-1},j-1}$$

void dfs(int u,int fa){
    dep[u]=dep[fa]+1;
    for(int i=head[u];i;i=edge[i].pre){//从以u为起点的加入的最后一条边开始遍历,遍历所有以u为起点的边。
        int j=edge[i].v;//取出当前边的终点编号。
        if(j==fa)continue;//如果当前这条边的终点是以u为终点的边的起点,那么说明构成了一个环,不执行。
        f[j][0]=u;//跳2^0(1)步到达的点即为u。
        for(int k=1;(1<<k)<=dep[u];k++){
            f[j][k]=f[f[j][k-1]][k-1];//状态转移
        }
        dfs(j,u);//进入以j为起点的下一组边(下一层
    }
}

③. 倍增找公共祖先

设要寻找公共祖先的两个点的编号分别为 $x$,$y$。那么可以分为四步:

第一步:将 $x$ 换为深度较大的一个,方便操作。

第二步:按照 $2$ 的次幂的步数枚举,将 $x$ 跳至与 $y$ 同等的深度。

第三步:让 $x$ 与 $y$ 共同前进,但是不能让它们跳到它们的公共祖先上。

第四步:此时,$x$ 与 $y$ 都会恰恰位于它们最近公共祖先的下一层,因此只需要让它们再跳 $1$ 步即可。

代码如下所示

int lca(int x,int y){
    //第一步
    if(dep[x]<dep[y])swap(x,y);
    //第二步
    for(int i=19;i>=0;i--){
        if(dep[f[x][i]]>=dep[y]){
            x=f[x][i];
        }
    }
    //若此时已经找到祖先了,就return。
    if(x==y)return y;
    //第三步
    for(int i=19;i>=0;i--){
        if(f[x][i]!=f[y][i]){
            x=f[x][i];
            y=f[y][i];
        }
    }
    //第四步
    return f[y][0];
}

3. Tarjan 法

我不会QAQ

为什么用 Tarjan倍增怎么你了

想要极致的时间复杂度还得人造 vector

趁朱老师发现之前,开溜

三、例题

1.【模板】最近公共祖先(LCA)

平台 网址
洛谷 https://www.luogu.com.cn/problem/P3379
南海信息学 https://nh.51goc.com/studentCourse/class?groupId=301&courseId=198&contestId=9993#testTab=9994_2 第一题

模板题,直接上代码:

#include <bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct EDGE{
    int v,next;
}edge[N*2];
int n,m,root,head[N],dep[N],f[N][20],id=1;
void un(int u,int v){
    edge[id].v=v;
    edge[id].next=head[u];
    head[u]=id;
    id++;
}
void dfs(int u,int fa){
    dep[u]=dep[fa]+1;
    for(int i=head[u];i;i=edge[i].next){
        int j=edge[i].v;
        if(j==fa)continue;
        f[j][0]=u;
        for(int k=1;(1<<k)<=dep[u];k++){
            f[j][k]=f[f[j][k-1]][k-1];
        }
        dfs(j,u);
    }
}
int lca(int x,int y){
    if(dep[x]<dep[y])swap(x,y);
    for(int i=19;i>=0;i--){
        if(dep[f[x][i]]>=dep[y]){
            x=f[x][i];
        }
    }
    if(x==y)return y;
    for(int i=19;i>=0;i--){
        if(f[x][i]!=f[y][i]){
            x=f[x][i];
            y=f[y][i];
        }
    }
    return f[y][0];
}
int main(){
    scanf("%d%d%d",&n,&m,&root);
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        un(u,v);
        un(v,u); 
    }
    dfs(root,0);
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}
posted @ 2023-07-17 20:00  Garbage_fish  阅读(59)  评论(0)    收藏  举报  来源