蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:倍增法求最近公共祖先LCA解析

一、引言

在少儿编程的算法进阶路上,最近公共祖先(LCA,Lowest Common Ancestor) 是一道绕不开的基础题。无论是蓝桥杯软件赛(C/C++·Python 程序设计)的树型模拟,还是全国青少年信息素养大赛算法应用主题赛里的家谱、族谱、组织关系题,又或是电子学会软件编程(C/C++ / Python)中高级别的树与递归章节,LCA 都是高频考点。

所谓「最近公共祖先」:在一棵以某个节点为根的树里,两个节点 u、v 向上追溯,最早交汇的那个祖先就叫它们的 LCA。比如家谱里,你和你表哥最近的共同长辈就是你们的祖父。

本文用一道原创题「校园家谱寻根」带大家吃透 倍增法求 LCA,给出 C++ / Python 双版代码、复杂度分析、七个易错点和进阶玩法。

二、题目 / 项目目标

【原创题】校园家谱寻根

学校要整理一份家族族谱,把 n 个人按「父子」关系连成一棵以 1 号(开校始祖)为根的树。现在要回答 m 次询问:给定两个人 a 和 b,他们最近的共同祖先是谁?

输入格式 :第一行 n、m;接下来 n−1 行每行两个正整数 u v,表示 u 是 v 的父亲(即 u 到 v 有一条父子边);接下来 m 行每行两个正整数 a b,表示一次询问。

输出格式 :对每次询问,输出 a 和 b 的最近公共祖先编号。

数据范围:1 ≤ n, m ≤ 10^5。

样例(n=7,边为 1-2、1-3、2-4、2-5、3-6、5-7):

询问 答案
LCA(4, 5) 2
LCA(6, 7) 1
LCA(4, 7) 2
LCA(7, 6) 1
LCA(4, 4) 4
LCA(1, 7) 1

用肉眼也能验证:4 和 5 都是 2 的孩子,最近公共祖先就是 2;6 在 3 这一支、7 在 5→2 这一支,两支路只在根 1 处交汇,所以 LCA=1。

三、核心考点

  1. 倍增思想(Binary Lifting) :用 up[i][j] 表示「节点 i 的 2^j 级祖先」,把「向上跳任意步」拆成「若干次 2 的幂次跳跃」,类似二进制拆分。
  2. 树的深度与父节点 :先用一次遍历(BFS 或 DFS)确定每个节点的直接父亲 parent 和深度 depth。
  3. 倍增表递推 :up[i][j] = up[ up[i][j-1] ][j-1],即「跳 2^(j-1) 再跳 2^(j-1) = 跳 2^j」。
  4. 查询两步走 :先把深的节点抬到和浅的节点同一深度(按 diff 的二进制逐位跳),再让两个节点一起从大到小倍增上跳,直到它们的父亲相同------这个共同的父亲就是 LCA。

四、解法 / 拆解

第一步:建树 + 预处理父节点与深度

无向树要双向加边,从根 1 出发 BFS(或 DFS),遇到未访问的邻居就把它记为当前节点的孩子、深度 +1。这样每个节点(除根外)都有唯一父亲。

第二步:构建倍增表 up

开一个二维数组 up[n+1][LOG+1],其中 LOG = n.bit_length()(保证能跳到任意高度)。

  • up[i][0] = parent[i](1 级祖先就是直接父亲)
  • 递推:up[i][j] = up[ up[i][j-1] ][j-1]

第三步:查询 LCA

text 复制代码
函数 lca(u, v):
    1. 若 depth[u] < depth[v],交换使 u 更深
    2. 让 u 向上跳 (depth[u] - depth[v]) 步,与 v 同深
       把 diff 按二进制拆开:若 diff 的第 j 位为 1,则 u = up[u][j]
    3. 若此时 u == v,直接返回 u(一个本来就是另一个的祖先)
    4. 从大到小枚举 j:
         若 up[u][j] != up[v][j](还没相遇),则 u、v 一起跳 up[u][j]
    5. 循环结束后 u、v 的父亲相同,返回 up[u][0]

为什么第 4 步要「从大到小、且只跳到不相等的地方」?因为我们要找到「刚好在 LCA 正下方的那一对节点」:从 2^LOG 级开始尝试,能跳就跳,最后保证 upu0==upv0 但 u≠v,说明它们再跳一步就重合,所以共同父亲 up[u][0] 就是 LCA。

C++ 代码(已对拍验证)

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, q;
    if (!(cin >> n >> q)) return 0;
    vector<vector<int>> adj(n + 1);
    for (int i = 0; i < n - 1; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    int LOG = 0, t = n;
    while (t) { LOG++; t >>= 1; }          // LOG = ceil(log2(n)) 的位数
    vector<vector<int>> up(n + 1, vector<int>(LOG + 1, 0));
    vector<int> depth(n + 1, 0), parent(n + 1, 0);
    vector<bool> vis(n + 1, false);

    // 1) BFS 建树,记录父亲与深度
    queue<int> dq;
    dq.push(1); vis[1] = true; depth[1] = 0; parent[1] = 0;
    while (!dq.empty()) {
        int u = dq.front(); dq.pop();
        for (int v : adj[u]) {
            if (!vis[v]) {
                vis[v] = true; parent[v] = u;
                depth[v] = depth[u] + 1; dq.push(v);
            }
        }
    }

    // 2) 倍增表
    for (int i = 1; i <= n; i++) up[i][0] = parent[i];
    for (int j = 1; j <= LOG; j++)
        for (int i = 1; i <= n; i++)
            up[i][j] = up[ up[i][j-1] ][j-1];

    // 3) 查询
    auto lca = [&](int u, int v) -> int {
        if (depth[u] < depth[v]) swap(u, v);
        int diff = depth[u] - depth[v];
        for (int j = 0; j <= LOG; j++)          // 先对齐深度(小到大)
            if (diff & (1LL << j)) u = up[u][j];
        if (u == v) return u;
        for (int j = LOG; j >= 0; j--)            // 再一起跳(大到小)
            if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; }
        return up[u][0];
    };

    while (q--) {
        int u, v; cin >> u >> v;
        cout << lca(u, v) << "\n";
    }
    return 0;
}

Python 代码(已对拍验证)

python 复制代码
import sys
from collections import deque

def main():
    data = sys.stdin.read().strip().split()
    if not data:
        return
    it = iter(data)
    n = int(next(it)); q = int(next(it))
    adj = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u = int(next(it)); v = int(next(it))
        adj[u].append(v); adj[v].append(u)

    LOG = n.bit_length()
    up = [[0] * (LOG + 1) for _ in range(n + 1)]
    depth = [0] * (n + 1)
    parent = [0] * (n + 1)
    vis = [False] * (n + 1)

    # 1) BFS 建树
    dq = deque([1]); vis[1] = True; depth[1] = 0
    while dq:
        u = dq.popleft()
        for v in adj[u]:
            if not vis[v]:
                vis[v] = True; parent[v] = u
                depth[v] = depth[u] + 1; dq.append(v)

    # 2) 倍增表
    for i in range(1, n + 1):
        up[i][0] = parent[i]
    for j in range(1, LOG + 1):
        for i in range(1, n + 1):
            up[i][j] = up[up[i][j - 1]][j - 1]

    out = []
    for _ in range(q):
        u = int(next(it)); v = int(next(it))
        if depth[u] < depth[v]:
            u, v = v, u
        diff = depth[u] - depth[v]
        for j in range(LOG + 1):          # 对齐深度
            if diff & (1 << j):
                u = up[u][j]
        if u != v:
            for j in range(LOG, -1, -1):   # 一起跳
                if up[u][j] != up[v][j]:
                    u = up[u][j]; v = up[v][j]
            u = up[u][0]
        out.append(str(u))
    sys.stdout.write("\n".join(out))

if __name__ == "__main__":
    main()

五、时间 / 空间复杂度

  • 预处理 :BFS 遍历 O(n),倍增表 O(n·LOG),合计 O(n log n)。
  • 单次查询 :对齐深度 O(log n),一起跳 O(log n),合计 O(log n)。
  • 总复杂度 :O((n + m) log n),对 n、m 到 10^5 完全轻松。
  • 空间:倍增表 O(n log n),其余 O(n)。

这套复杂度比「每次暴力沿父链向上爬」的 O(n·m) 好太多,是蓝桥杯、信息素养大赛中树题的标准解法。

六、七个易错点

  1. 根的上方要统一设 0 :up[1][j] 一律为 0。当两个节点都跳到根之上时,up[u][j] == up[v][j] == 0 相等,循环自然停止,不会数组越界。
  2. 倍增表第二维要够大 :必须开到 ⌈log₂n⌉+1。用 n.bit_length() 最稳,手工估计 ceil(log2(n))+1 经常少开一格导致跳大步时越界。
  3. 两次跳的位序相反 :对齐深度时 j 从 0 到 LOG(小到大,按 diff 的二进制逐位落子);一起跳时 j 必须从 LOG 递减到 0(大到小,保证不越过 LCA)。顺序写反就退化成 O(n)。
  4. 漏掉 u==v 特判:对齐深度后若两者已相等,应直接返回,否则会多跳一步、把答案错改成根。
  5. 无向树建图 + 防回退 :边要双向加,但 BFS/DFS 必须靠 vis 或 parent 避免走回父亲,否则死循环或重复建出假子树。
  6. 深度基准要统一 :根深度建议从 0 起(或都从 1 起),但 up[i][0]=parent 与 depth 必须是同一套根定义,混用会让查询结果整体错位。
  7. 下标 0 基 / 1 基 :题目节点常从 1 起,数组开 n+1;若样例是 0 基(root=0),up[0][j]=0 逻辑不变,但要整体平移,最易在「样例过了、提交 WA」时栽跟头。

七、进阶

  • 树上两点距离 :dist(u, v) = depth[u] + depth[v] − 2·depth[lca(u, v)],直接套用上面的 LCA,蓝桥杯「树上最远两个景点」类题的核心子程序。
  • 树上差分(路径整体 +1) :把路径 u--v 上每个点加 1,等价于 diff[u]++、diff[v]++、diff[lca]--、diff[parent[lca]]--,最后做一次子树求和即可,常见于「统计每条边被多少条路径经过」。
  • 树链剖分求 LCA:重链剖分后沿重链向上跳,单次 O(log n) 且不用倍增表,适合同时要做「路径权值修改 / 查询」的题。
  • 欧拉序 + ST 表(RMQ):DFS 得到欧拉序,LCA 即区间内深度最小的节点,O(n log n) 预处理、O(1) 查询,适合超多询问的场景。
  • k 级祖先 :up[u][j] 直接取第 2^j 级祖先,回指本号此前讲过的「树上倍增求 k 级祖先」一文,二者是同一张表的两种用法。

八、小结与互动

倍增法求 LCA 的精髓就一句话:一张 up 倍增表 + 「先对齐深度、再一起从大到小跳」。它把树上的祖先查询从线性压到对数级,是树类题目的万能底座------后面学的树上距离、树上差分、LCA 应用( recent common ancestor 在族谱/组织关系/网络拓扑里的变形)全都建立在它之上。

蓝桥杯报名正在火热进行(软件赛报名 2026-10-08---12-18,省赛 2027-04-10),电子学会 12 月软件编程(C/C++ 一级至十级、Python 一级至六级)考级报名也已开启(9/23---12/2),全国青少年信息素养大赛算法应用主题赛同步推进------把 LCA 这个模板练熟,正是冲刺这些赛事树型考点的关键一步。

互动时间:你在刷题时还遇到过哪些「树」的题型?是树上最远距离、最近公共祖先、还是子树统计?留言说说,下一篇我们可以接着拆「树上差分」或「树链剖分」。觉得有用记得点赞收藏,关注不迷路~


📚 免费少儿编程资料(夸克网盘领取)

以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
冻柠檬飞冰走茶1 小时前
PTA刷题集之Battle Over Cities - Hard Version(C语言实现)
c语言·开发语言·数据结构·算法
吴长建先生重名了1 小时前
电商订单系统在Docker和Kubernetes中部署的高
算法
atwednesday1 小时前
一文理解python中的task,协程,woker
python
雷✘2 小时前
C++ 类与对象的初始化机制、成员访问边界与拷贝优化
java·网络·c++
用户0332126663672 小时前
使用 Python 在 Excel 中添加与删除筛选器
python·excel
1M552 小时前
AI应用后端工程(03):回答输出一半就断了:AI 流式接口如何定义成功与失败?
python·面试·职场和发展·计算机专业·agent开发
努力实干2 小时前
简单工厂模式(Simple Factory Pattern)——C++
开发语言·c++·简单工厂模式
AI 编程助手GPT3 小时前
GPT 排的日志时间线差一小时?用 zoneinfo 复现夏令时回拨
人工智能·python·gpt
Thanks_ks3 小时前
【第 003 讲】全面认识 Python:语言起源与设计哲学 | 行业地位与流行趋势 | 技术优势与应用领域 | 技术边界与工程局限
人工智能·python·数据分析·编程语言·python 入门·web 开发·python 之禅