GESP 8级

最近公共祖先 LCA

8级 · 高级数据结构与DP
最近公共祖先(Lowest Common Ancestor, LCA)是指在树中,两个节点的最近的共同祖先——就是两个节点"向上走"最先相遇的那个节点。常用倍增法在 O(log n) 时间内求解。
💡 这是什么?
家族树比喻:想象一个家谱。你和你的表哥的"最近公共祖先"是谁?就是你们共同的爷爷(或外公)。再比如,你和你爸爸的最近公共祖先就是你爸爸自己。

正式定义:在有根树中,节点 a 和节点 b 的 LCA 是同时满足以下条件的节点 c:
① c 是 a 的祖先(c 在从 a 到根的路径上)
② c 是 b 的祖先
③ c 的深度尽可能大(即尽可能"低",最靠近 a 和 b)

倍增法核心思路:
① 先用 BFS 预处理出每个节点的深度 depth 和 2^j 级祖先 fa[i][j]
② 求 LCA 时:先让深的节点跳到和浅的节点同一深度,然后两个一起向上跳
🌟 为什么重要?
• 树上距离:a 到 b 的距离 = depth[a] + depth[b] - 2×depth[LCA(a,b)]
• 路径查询:很多树上问题需要找两个节点的路径,而路径必然经过它们的 LCA
• 竞赛高频:树链剖分、虚树等高级技巧都以 LCA 为基础
• 实际应用:版本控制系统(git merge)中,合并两个分支就是找它们的 LCA
• GESP 8级必考:倍增法 LCA 是必须掌握的
📋 前置知识(学这个之前你需要知道)
1. 树的基本概念(GESP 5-6级):有根树、父节点、子节点、深度、祖先
2. BFS 广度优先搜索(GESP 3级):用来预处理 depth 和 fa 数组
3. 二进制分解思想:任何正整数都可以表示为若干个 2 的幂次之和
4. 邻接表存储树:树是无环连通图,用邻接表存储
📐 倍增法核心公式
fa[i][j] = 节点 i 的第 2^j 级祖先

递推公式:fa[v][j] = fa[ fa[v][j-1] ][j-1]
含义:v 的第 2^j 级祖先 = (v 的第 2^(j-1) 级祖先) 的第 2^(j-1) 级祖先

比如:fa[v][3] = fa[ fa[v][2] ][2]
v 的第8级祖先 = (v的第4级祖先)的第4级祖先

j 最多取到 log₂(n),比如 n=100000 时 j 最大取 17
💻 完整代码(带详细注释)
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int N = 100010;
const int LOG = 20;  // log₂(100000) ≈ 17,开到20够用

vector<int> adj[N];  // 邻接表存树
int depth[N];         // depth[i] = 节点 i 的深度(根为1)
int fa[N][LOG];       // fa[i][j] = 节点 i 的第 2^j 级祖先

// BFS 预处理:计算 depth 和 fa 数组
void bfs_lca(int root) {
    queue<int> q;
    q.push(root);
    depth[root] = 1;  // 根节点深度设为1(方便判断,depth=0表示未访问)

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        // 遍历 u 的所有邻居(子节点)
        for (int v : adj[u]) {
            if (depth[v]) continue;  // 已访问过,跳过(v是u的父节点)
            depth[v] = depth[u] + 1;  // 子节点深度 = 父节点深度 + 1
            fa[v][0] = u;  // v 的第 1 级祖先(2^0=1)就是 u
            // 倍增:预处理 v 的所有 2^j 级祖先
            for (int j = 1; j < LOG; j++) {
                // v 的第 2^j 级祖先 = (v的第 2^(j-1) 级祖先)的第 2^(j-1) 级祖先
                fa[v][j] = fa[ fa[v][j-1] ][j-1];
            }
            q.push(v);
        }
    }
}

// 求节点 a 和 b 的最近公共祖先
int lca(int a, int b) {
    // 第1步:确保 a 是较深的节点(如果 a 比 b 浅就交换)
    if (depth[a] < depth[b]) swap(a, b);

    // 第2步:把 a 向上跳,跳到和 b 同一深度
    // 从大到小枚举 j,利用二进制分解
    for (int j = LOG - 1; j >= 0; j--) {
        // 如果 a 跳 2^j 步后仍然比 b 深(或同样深),就跳
        if (depth[fa[a][j]] >= depth[b])
            a = fa[a][j];  // a 向上跳 2^j 步
    }

    // 特殊情况:如果跳到同一深度后 a==b,说明 b 就是 a 的祖先
    if (a == b) return a;

    // 第3步:a 和 b 同一深度但不是同一个点
    // 一起向上跳,跳到 LCA 的下一层
    for (int j = LOG - 1; j >= 0; j--) {
        if (fa[a][j] != fa[b][j]) {
            // 如果跳 2^j 步后两个节点不同,说明 LCA 还在上面
            a = fa[a][j];  // a 向上跳
            b = fa[b][j];  // b 向上跳
        }
    }
    // 此时 a 和 b 的父节点就是 LCA
    return fa[a][0];
}

int main() {
    int n, q;  // n 个节点,q 次查询
    cin >> n >> q;
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);  // 无向树,双向加边
        adj[v].push_back(u);
    }
    bfs_lca(1);  // 以节点1为根,预处理

    while (q--) {
        int a, b;
        cin >> a >> b;
        cout << lca(a, b) << endl;
    }
    return 0;
}
🔍 倍增法执行过程举例
树:
   1 (depth=1)
  / \
 2  3 (depth=2)
 /
4 (depth=3)

预处理 fa 数组:
   fa[2][0]=1, fa[2][1]=fa[fa[2][0]][0]=fa[1][0]=0
   fa[3][0]=1, fa[3][1]=fa[fa[3][0]][0]=fa[1][0]=0
   fa[4][0]=2, fa[4][1]=fa[fa[4][0]][0]=fa[2][0]=1, fa[4][2]=fa[fa[4][1]][1]=fa[1][1]=0

求 LCA(4, 3):
   depth[4]=3 > depth[3]=2,a=4, b=3
   Step 2:让 a 跳到和 b 同深度
      j=1: depth[fa[4][1]]=depth[1]=1 < depth[b]=2 → 不跳
      j=0: depth[fa[4][0]]=depth[2]=2 ≥ depth[b]=2 → a=fa[4][0]=2
   现在 a=2, b=3,a≠b
   Step 3:一起向上跳
      j=1: fa[2][1]=0 == fa[3][1]=0 → 不跳
      j=0: fa[2][0]=1 ≠ fa[3][0]=1? → 实际都是1,不跳
   return fa[a][0] = fa[2][0] = 1
   LCA(4,3) = 1 ✓
⏱ 复杂度分析
预处理(BFS + 倍增):O(n × log n)
每次查询:O(log n)
总复杂度:O((n + q) × log n),q 是查询次数

空间:O(n × log n),fa 数组大小

比暴力法 O(n) 每次查询快得多!
📐 常见衍生问题
树上两点距离:
dist(a, b) = depth[a] + depth[b] - 2 × depth[LCA(a,b)]

为什么这个公式成立?
a→LCA 的距离 = depth[a] - depth[LCA]
b→LCA 的距离 = depth[b] - depth[LCA]
两者相加 = depth[a] + depth[b] - 2×depth[LCA]
⚠️ 易错点
1. depth[0]=0 这个细节 → 未访问的节点 depth=0,根的 depth≥1。fa[根][j] 会指向 0,depth[0]=0 恰好能让条件 depth[fa[a][j]] >= depth[b] 正确工作。
2. fa 数组初始化 → 全局变量默认为0,所以 fa[根][j]=0 是正确的。不要手动初始化为 -1。
3. 交换后别搞混 → swap(a,b) 后,a 一定是较深的那个。
4. j 要从大到小枚举 → 从 LOG-1 到 0,先跳大步再跳小步,类似二进制分解。
5. 最后 return fa[a][0] → 不是 return a!跳完循环后 a 和 b 在 LCA 的下一层,它们的父节点才是 LCA。
6. 无向树要加双向边 → adj[u].push_back(v) 和 adj[v].push_back(u) 都要写。
🎯 练习建议
入门练习:
• 洛谷 P3379 【模板】最近公共祖先(LCA)(直接套模板)

进阶练习:
• 洛谷 P1395 会议(LCA + 树上距离)
• 洛谷 P4419 [NOI2018]你的名字(LCA应用)
• 洛谷 P2633 统计医生治疗快乐度(主席树 + LCA,非常难)

学习方法:
① 先手写 BFS 预处理 depth 和 fa[ ][0]
② 理解倍增公式 fa[v][j] = fa[ fa[v][j-1] ][j-1]
③ 分步理解 lca 函数:先调深度,再一起跳
④ 背下模板,考试时直接套用