最近公共祖先(LCA)

  • 2023-09-04 14:20:54
  • 阅读:68次

\(最近公共祖先(LCA)\)

闲谈

原因

这几年\(NOIP\)考树考的好多,打算写几篇博客来增强记忆。\(NOIP rp++\)

背景

在树上的问题中,对两个点展开的有很多,\(LCA\)在很多时候会起到很大的作用。

算法

概念

首先我们要谈谈什么是最近公共祖先:对于有根树\(T\)的两个结点\(u、v\),最近公共祖先\(LCA(T,u,v)\)表示一个结点\(x\),满足\(x\)\(u\)\(v\)的祖先且\(x\)的深度尽可能大。在这里,一个节点也可以是它自己的祖先。也可以理解为距离两个节点最近的公共祖先。所以它主要是用来处理当两个点有唯一一条确定的最短路径时的路径。这里我们就不介绍其他一些算法了,只谈倍增的方法。

倍增\(LCA\)

首先我们来理解一下什么是倍增,倍增就是按照\(2\)的倍数来增长,\(1, 2, 4, 8, 16, 32……\),但我们在用倍增求解\(LCA\)的时候并不从小到大,而是从大到小来进行,\(……32, 16, 8, 4, 2, 1\),我们用这样一个图来更好的理解

(图片来源于网络)
在这颗树中,\(17\)\(18\)\(LCA\)就是\(3\)
暴力的算法就是让两者分别向上一个一个窜,直到两者相遇。但是这个时间复杂度太大了,我们就需要用到倍增的算法了。
我们从大往小了跳,如果大了,那么我们就调小, 这回它的时间复杂度为\(O(nlogn)\)足够我们在比赛中使用了,那么该如何实现呢。
首先我们需要记录每个节点他们的深度\(depth[x]\)和他们的\(2^i\)级祖先\(fa[i][j]\)(表示节点\(i\)\(2^j\)级祖先)。

void dfs(int now, int fath){    //now表示当前节点,fath表示它的父亲节点
    fa[now][0] = fath;  //2的0次方为1,即它的父亲节点
    depth[now] = depth[fath] + 1;   //now的深度为它父亲的深度加1
    for(int i = 1; i <= lg[depth[now]]; i++)    //lg数组的大小是指当前节点的深度的以2为底的对数,我们回溯只能回溯到根节点,所以需要加上这个判断
        fa[now][i] = fa[fa[now][i - 1]][i - 1];
        //这是整个算法的核心,表示now的第2 ^ i个祖先为now的第2 ^ (i - 1)个祖先的2 ^ (i- 1)祖先
        //2 ^ i = 2 ^(i - 1) + 2 ^ (i - 1)
    for(int i = head[now]; i; i = e[i].next)    //我们用链式前向星来存储整个图
        if(e[i].to != fath) dfs(e[i].to, now);  //如果now当前节点指向的点不为它的父亲,那么则为它的儿子,这是我们now作为父节点再进行dfs,将它的子节点进行预处理
}

此时我们已经进行玩了预处理,这里我们先将\(lg\)数组求解一遍,进行常数优化。

for(int i = 1; i <= n; i++)
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);

在这之后就是倍增\(LCA\)了,我们先将两个点提到同一高度,再统一的跳。注意这里很重要,我第一遍学习的时候没有理解透彻就是卡在了这里。
但是我们不能直接跳到他们的\(LCA\),因为我们是从大到小进行跳的,可能最开始直接跳到了根节点,根节点肯定是两个点的祖先,但是不是我们要求的最短公共祖先,所以我们要往下再跳一层,发现诶两者不一样了,这样的话,这一层就是两者的最短公共祖先。

int LCA(int x, int y){
    if(depth[x] < depth[y]) swap(x, y); //我们在这里强制x的深度>=y的深度
    while(depth[x] > depth[y]) 
        x = fa[x][lg[depth[x] - depth[y]] - 1]; //首先将x和y跳到同一层
    if(x == y) return x;    //如果x是y的祖先,那么他们的LCA就是x
    for(int i = lg[depth[x]] - 1; i >= 0; i--) //不断的向上爬
        if(fa[x][i] != fa[y][i])      //我们要跳到的是它们的LCA的下一层,所以它们肯定不一样,如果不相等就跳过去
            x = fa[x][i], y = fa[y][i];
    return fa[x][0];    //返回父节点
}

所以在这个图中,我们按照倍增\(LCA\)的方法来求,路径为:
\(17 -> 10 -> 7 -> 3\)
\(18 -> 16 -> 8 -> 5 -> 3\)
所以这就是整个过程了。

代码实现

#include<cstdio>
#include<iostream>
#define N 500010
using namespace std;
int n, m, s, cnt = 0;
int lg[N], head[N  << 1];
int depth[N], fa[N][22];
struct edge{
    int next, to;
}e[N << 1];

void add(int u, int v){
    e[++cnt].next = head[u];
    e[cnt].to = v;
    head[u] = cnt;
}

void dfs(int now, int fath){    //now表示当前节点,fath表示它的父亲节点
    fa[now][0] = fath;  //2的0次方为1,即它的父亲节点
    depth[now] = depth[fath] + 1;   //now的深度为它父亲的深度加1
    for(int i = 1; i <= lg[depth[now]]; i++)    //lg数组的大小是指当前节点的深度的以2为底的对数,我们回溯只能回溯到根节点,所以需要加上这个判断
        fa[now][i] = fa[fa[now][i - 1]][i - 1];
        //这是整个算法的核心,表示now的第2 ^ i个祖先为now的第2 ^ (i - 1)个祖先的2 ^ (i- 1)祖先
        //2 ^ i = 2 ^(i - 1) + 2 ^ (i - 1)
    for(int i = head[now]; i; i = e[i].next)    //我们用链式前向星来存储整个图
        if(e[i].to != fath) dfs(e[i].to, now);  //如果now当前节点指向的点不为它的父亲,那么则为它的儿子,这是我们now作为父节点再进行dfs,将它的子节点进行预处理
}

int LCA(int x, int y){
    if(depth[x] < depth[y]) swap(x, y); //我们在这里强制x的深度>=y的深度
    while(depth[x] > depth[y]) 
        x = fa[x][lg[depth[x] - depth[y]] - 1]; //首先将x和y跳到同一层
    if(x == y) return x;    //如果x是y的祖先,那么他们的LCA就是x
    for(int i = lg[depth[x]] - 1; i >= 0; i--) //不断的向上爬
        if(fa[x][i] != fa[y][i])      //我们要跳到的是它们的LCA的下一层,所以它们肯定不一样,如果不相等就跳过去
            x = fa[x][i], y = fa[y][i];
    return fa[x][0];    //返回父节点
}

int main(){
    scanf("%d %d %d", &n, &m, &s);
    for(int i = 1; i < n; i++){
        int u, v; scanf("%d %d", &u, &v);
        add(u, v); add(v, u);
    }
    for(int i = 1; i <= n; i++)
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
    dfs(s, 0);
    for(int i = 1; i <= m; i++){
        int u, v; scanf("%d %d", &u, &v);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}

以上就是用倍增来实现\(LCA\)的方法了,只有勤加练习,才能够对其有更深刻的理解和掌握。
完结撒花ヾ(✿゚▽゚)ノ

热门内容