LCA学习笔记

发布时间 2023-10-16 15:22:49作者: 2k3

定义

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

求法

有多种求法,目前就学习了倍增和 dfs 序求 LCA ,等后面学新的了再加上。

前置知识: ST 表,dfs 序。

为方便说明,下面全都是求 \(x\)\(y\) 的 LCA,设其为 \(z\)

朴素算法

\(x\) 的深度大于 \(y\) 的深度。先将 \(x\) 上移到与 \(y\) 同一深度。再将 \(x\)\(y\) 同时上移,当 \(x=y\) 时,即为 \(z\)

需要 dfs 预处理出深度。预处理 \(O(n)\) ,单次查询 \(O(n)\)

倍增算法

是朴素算法的优化,运用倍增思想,预处理出 \(an[k][x]\) 表示节点 \(x\)\(2^k\) 级祖先,用 \(an[k][x]\) 来向上跳。

\(an[0][x]\) 等于 \(x\) 的父节点,其余令 \(an[k][x]=an[k-1][an[k-1][x]]\) ,即 \(x\) 的第 \(2^k\) 个祖先是 \(x\) 的第 \(2^{k-1}\) 个祖先的第 \(2^{k-1}\) 个祖先。

预处理 \(O(n \log n)\) ,查询 \(O(\log n)\)

模板题的代码:

点击查看代码
#include <iostream>
#include <cmath>
#define int long long
using namespace std;
const int Max = 5e5+10;
int n,m,root;
int an[25][Max];
int dep[Max];
struct EDGE{
    int Head[Max*2],Next[Max*2],Ver[Max*2],tot;
    inline void add(int x,int y){
        Ver[++tot]=y,Next[tot]=Head[x],Head[x]=tot;
    }
}E;
void build(int x,int fa)
{
    dep[x]=dep[fa]+1;
    for(int i=E.Head[x];i;i=E.Next[i]){
        int y=E.Ver[i];
        if(y==fa)continue;
		an[0][y]=x; 
		for(int j = 1;j <= __lg(n);j++){
        	an[j][y]=an[j-1][an[j-1][y]];
    	}
        build(y,x);
    }
}
inline int LCA(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    while(dep[x]!=dep[y])
        x=an[__lg(dep[x]-dep[y])][x];
    if(x==y)return x;
    for(int i=__lg(n);i>=0;i--){
        if(an[i][x]!=an[i][y]) x=an[i][x],y=an[i][y];
    }
    return an[0][x];
}
inline int read(){
    int num=0,fl=1;char c=getchar();
    while(c<'0'||c>'9'){
        if(c=='-') fl=-1;
        c=getchar();
    }
    while(c >='0'&&c <='9'){
        num=(num<<3)+(num<<1)+(c^48);
        c=getchar();
    }
    return num*fl;
}
signed main(){
    n=read(),m=read(),root=read();
    for(int i = 1;i < n;i++){
        int x=read(),y=read();
        E.add(x,y);
        E.add(y,x);
    }
    build(root,0);
    while(m--){
        int x=read(),y=read();
        printf("%lld\n",LCA(x,y));
    }
    return 0;  
} 

DFS序求LCA

定义

DFS 序:对一棵树进行 DFS 后得到的节点序列。在 DFS 序中,祖先节点一定在后代节点前。

dfn :节点在 DFS 中被遍历到的顺序。

过程

\(x\) 的 dfn 小于 \(y\) 的 dfn。

\(x\) 不是 \(y\) 的祖先时,\(d\) 一定不在 \(dfn[x]\)\(dfn[y]\) 的序列中。设 \(u\)\(z\) 的儿子且子树中包含 \(y\) ,则 \(dfn[u]\) 一定在 \(dfn[x]\)\(dfn[y]\) 的序列中(具体可以画个图看看)。这就说明当 \(x\) 不是 \(y\) 的祖先时,只需求 \(dfn[x]\)\(dfn[y]\) 的序列中深度最小的任意一个点,它的父节点就是 \(z\) 。这就转化为了 RMQ 问题,可以用 st 表来求解。

\(x\)\(y\) 的祖先时,我们可以求 \(dfn[x+1]\)\(dfn[y]\) 的序列中深度最小的任意一个点,其父亲就是 \(z\),即 \(x\)

唯一需要特判的情况是当 \(x=y\) 时。

预处理ST表 \(O(n\log n)\) ,单次查询 \(O(1)\)

模板题的代码:

点击查看代码
#include <iostream>
#include <cmath>
#define int long long
using namespace std;
const int Max = 5e5+10;
int n,m,root;
int dfn[Max],tot;
struct EDGE{
    int Head[Max*2],Next[Max*2],Ver[Max*2],tot;
    inline void add(int x,int y){
        Ver[++tot]=y,Next[tot]=Head[x],Head[x]=tot;
    }
}E;
namespace st{
    int st[25][Max];
    inline int Min(int x,int y){return dfn[x]<dfn[y]?x:y;}
    inline void build()
    {
        for(int j = 1;j <= __lg(n);j++)
            for(int i = 1;i+(1<<j)-1<=n;i++)
                st[j][i]=Min(st[j-1][i],st[j-1][i+(1<<j-1)]);
    }
    inline int getMin(int l,int r)
    {
        int k=__lg(r-l+1);
        return Min(st[k][l],st[k][r-(1<<k)+1]);
    }
};
void dfs(int x,int fa){
    dfn[x]=++tot;
    st::st[0][dfn[x]]=fa;
    for(int i = E.Head[x];i;i=E.Next[i]){
        int y=E.Ver[i];
        if(y==fa)continue;
        dfs(y,x);
    }
}
inline int LCA(int x,int y){
    if(x==y)return x;
    x=dfn[x],y=dfn[y];
    if(x>y) swap(x,y);
    return st::getMin(x+1,y);
}
inline int read(){
    int num=0,fl=1;char c=getchar();
    while(c<'0'||c>'9'){
        if(c=='-') fl=-1;
        c=getchar();
    }
    while(c >='0'&&c <='9'){
        num=(num<<3)+(num<<1)+(c^48);
        c=getchar();
    }
    return num*fl;
}
signed main(){
    n=read(),m=read(),root=read();
    for(int i = 1;i < n;i++){
        int x=read(),y=read();
        E.add(x,y);
        E.add(y,x);
    }
    dfs(root,0);
    st::build();
    while(m--){
        int x=read(),y=read();
        printf("%lld\n",LCA(x,y));
    }
    return 0;  
}