算法学习:最近公共祖先(LCA)

详细讲解 LCA
希望能帮助你掌握 LCA

一、问题引入与基本概念

1.1 直观理解LCA

想象一个家族族谱,我们想知道两个人最近的共同祖先是谁。例如,表兄弟两人的最近共同祖先是他们的祖父,而不是曾祖父。在树形结构中,这个概念被形式化为"最近公共祖先"(LCA)。

让我们通过一个具体例子来理解:

        A
       / \
      B   C
     / \   \
    D   E   F
   / \     / \
  G   H   I   J

在这棵树中:

  • 节点G和H的LCA是D
  • 节点G和E的LCA是B
  • 节点I和J的LCA是F
  • 节点D和F的LCA是A

1.2 形式化定义

给定一棵有根树T和两个节点u和v,LCA(u,v)是满足以下条件的节点x:

  1. x是u的祖先
  2. x是v的祖先
  3. 在T的所有满足上述两个条件的节点中,x的深度最大

二、解决LCA问题的基本方法

2.1 朴素算法

最直观的方法是:

  1. 从u节点向上遍历到根,记录所有祖先节点
  2. 从v节点向上遍历到根,记录所有祖先节点
  3. 找出两个祖先序列中最后一个相同的节点

这种方法的时间复杂度为O(n),在最坏情况下(如退化成链表)需要遍历整棵树。

2.2 代码实现(朴素方法)

#include<bits/stdc++.h>
using namespace std;
struct node{
    int val;
    node*pa;
    node(int x):val(x),pa(nullptr){}
};
node*a[1005],*b[1005];
int cnt1,cnt2;
void ga(node*u){
    cnt1=0;
    while(u!=nullptr){
        a[++cnt1]=u;
        u=u->pa;
    }
}
void gb(node*v){
    cnt2=0;
    while(v!=nullptr){
        b[++cnt2]=v;
        v=v->pa;
    }
}
node*fl(node*u,node*v){
    ga(u);
    gb(v);
    node*lca=nullptr;
    int i=cnt1,j=cnt2;
    while(i>=1&&j>=1&&a[i]==b[j]){
        lca=a[i];
        i--;
        j--;
    }
    return lca;
}

三、优化算法:二进制提升法

3.1 算法思想

朴素算法的效率不高,我们可以通过预处理来加速查询。二进制提升法(Binary Lifting)是一种使用动态规划预处理每个节点的2^k级祖先的方法,使得每次查询可以在O(logn)时间内完成。

3.2 预处理阶段

我们定义一个二维数组up[u][k],表示节点u的2^k级祖先。这个数组可以通过动态规划来填充:

  • up[u][0] = parent[u] (直接父节点)
  • up[u][k] = up[up[u][k-1]][k-1] (递归定义)

3.3 查询阶段

查询LCA(u,v)的步骤:

  1. 将u和v调整到同一深度
  2. 从最大的k开始尝试向上跳跃,直到找到LCA

3.4 代码实现

#include<bits/stdc++.h>
using namespace std;
const int maxn=1000;
const int lg=log2(maxn)+1;
int g[maxn][maxn],cnt[maxn];
int up[maxn][lg];
int dep[maxn];
void dfs(int u,int fa){
    up[u][0]=fa;
    for(int i=1;i<lg;i++)
        up[u][i]=up[up[u][i-1]][i-1];
    for(int i=1;i<=cnt[u];i++){
        int v=g[u][i];
        if(v!=fa){
            dep[v]=dep[u]+1;
            dfs(v,u);
        }
    }
}
int lca(int u,int v){
    if(dep[u]<dep[v])swap(u,v);
    for(int i=lg-1;i>=0;i--)
        if(dep[u]-(1<<i)>=dep[v])
            u=up[u][i];
    if(u==v)return u;
    for(int i=lg-1;i>=0;i--)
        if(up[u][i]!=up[v][i]){
            u=up[u][i];
            v=up[v][i];
        }
    return up[u][0];
}

四、可视化示例

让我们通过一个具体的树结构来理解二进制提升法:

A
B
C
D
E
F
G
H
I
J

预处理后的up数组(部分):

节点up[][0]up[][1]up[][2]
A---
BA--
CA--
DBA-
EBA-
FCA-
GDBA
HDBA
IFCA
JFCA

查询LCA(G, I)的过程:

  1. depth[G]=3, depth[I]=3(已经同深度)
  2. 比较up[G][1]=B ≠ up[I][1]=C
  3. 比较up[G][0]=D ≠ up[I][0]=F
  4. 比较up[D][0]=B ≠ up[F][0]=C
  5. 比较up[B][0]=A == up[C][0]=A
  6. 返回A

五、算法分析与比较

5.1 时间复杂度

  1. 朴素算法:

    • 预处理:无
    • 查询:O(n)每次查询
  2. 二进制提升法:

    • 预处理:O(nlogn)(DFS遍历每个节点,对每个节点计算logn个祖先)
    • 查询:O(logn)每次查询

5.2 空间复杂度

二进制提升法需要O(nlogn)的额外空间存储up数组。

5.3 适用场景

  • 朴素算法适用于查询次数少或树结构经常变化的场景
  • 二进制提升法适用于需要大量查询且树结构不变的场景

六、其他LCA算法简介

除了二进制提升法,还有几种常见的LCA算法:

  1. Tarjan离线算法:使用并查集,在一次遍历中处理所有查询
  2. RMQ转换法:将LCA问题转化为区间最小值查询问题
  3. 树链剖分法:通过重链轻链分解来加速查询

七、实际应用示例

7.1 计算树上两点间距离

利用LCA可以高效计算树上任意两点间的距离:

int td(int u,int v,TreeNode* root){
    TreeNode* ancestor=findLCA(u,v);
    return depth[u]+depth[v]-2*depth[ancestor];
}

7.2 检测节点关系

判断一个节点是否是另一个节点的祖先:

bool check(TreeNode* u,TreeNode* v){
    // u是否是v的祖先
    TreeNode* ancestor=findLCA(u,v);
    return ancestor==u;
}

八、模板

模板题目

模板而已,前面讲很多了,这里就不做过多讲解了

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5,L=20;
int n,m,s,cnt;
int h[N],to[N<<1],ne[N<<1];
int d[N],f[N][L];
void add(int u,int v){
    to[++cnt]=v,ne[cnt]=h[u],h[u]=cnt;
}
void dfs(int u,int fa){
    f[u][0]=fa;
    for(int i=1;i<L;i++)
        f[u][i]=f[f[u][i-1]][i-1];
    for(int i=h[u];i;i=ne[i]){
        int v=to[i];
        if(v!=fa){
            d[v]=d[u]+1;
            dfs(v,u);
        }
    }
}
int lca(int u,int v){
    if(d[u]<d[v])swap(u,v);
    for(int i=L-1;i>=0;i--)
        if(d[u]-(1<<i)>=d[v])
            u=f[u][i];
    if(u==v)return u;
    for(int i=L-1;i>=0;i--)
        if(f[u][i]!=f[v][i])
            u=f[u][i],v=f[v][i];
    return f[u][0];
}
int main(){
    cin>>n>>m>>s;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        add(u,v),add(v,u);
    }
    dfs(s,s);
    while(m--){
        int a,b;
        cin>>a>>b;
        cout<<lca(a,b)<<'\n';
    }
    return 0;
}

九、总结

LCA问题是树结构中的一个基本问题,有着广泛的应用。我们从最直观的朴素算法出发,逐步介绍了更高效的二进制提升法,通过预处理将查询时间从 O(n) 优化到 O(logn)。理解LCA算法不仅能帮助我们解决具体问题,也是学习更复杂树算法的基础。

留下个赞,留下个收藏关注再走吧~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值