V

算法可视化

ZHEN
分享到微博分享到 XGitHub 仓库个人主页
学习工具算法复杂度速查算法学习路径
数据结构数组链表栈队列树堆哈希表图字典树并查集LRU 缓存跳表线段树B+ 树布隆过滤器树状数组
经典排序算法冒泡排序鸡尾酒排序双调排序选择排序插入排序二分插入排序希尔排序归并排序自顶向下归并快速排序三路快排双轴快排堆排序计数排序基数排序桶排序
图算法Dijkstra 最短路Kruskal 最小生成树Prim 最小生成树Bellman-Ford 最短路拓扑排序Floyd 多源最短路强连通分量2-SAT最大流二分图匹配LCA 倍增欧拉路径
动态规划编辑距离0-1 背包完全背包最长公共子序列最长递增子序列硬币找零方案数石子合并旅行商 TSP树形 DP数位 DP换根 DP
回溯与搜索N 皇后子集生成全排列组合总和迷宫寻路岛屿数量单词搜索数独A* 寻路
字符串KMP 字符串匹配Rabin-KarpBoyer-MooreManacher后缀数组LCP / height 数组AC 自动机Z 函数
数学与数论埃氏筛线性筛欧几里得算法快速幂扩展欧几里得中国剩余定理欧拉函数米勒-拉宾FFTPollard's Rho
计算几何凸包旋转卡壳最近点对线段相交扫描线求交
查找二分查找二分边界旋转数组搜索二分答案三分查找

LCA 倍增(最近公共祖先)

图算法 · 倍增跳表 · 家谱的交汇处

家谱往上翻,翻到第一个共同祖先

树上两点的最近公共祖先(Lowest Common Ancestor):同时是两点祖先、且深度最大的那个点——树上距离、路径查询全靠它。朴素做法让两点逐步爬父链,一次查询 O(n);查询一多就顶不住。本页的树:

        0
       / \
      1   2
     / \    \
    3   4    5
    |
    6
    |
    7

倍增:跳 1 步、2 步、4 步的跳表

预处理 up[k][u] = u 往上跳 2^k 步的祖先,「爸爸的爸爸」递推:up[k][u] = up[k−1][up[k−1][u]](跳 2^k = 先跳 2^(k−1) 再跳 2^(k−1))。查询三段式:①深的先对齐——深度差按二进制拆成若干次跳;②相同即答案;③否则从高位试跳——祖先不同才跳(相同就跳可能越过 LCA),最后两点停在 LCA 的两个孩子上,父亲即答案。下表 8 行 × 4 列就是跳表,黄格是每一步引用的格子;右侧代码随步同步高亮。

depthup⁰up¹up²
0
1
2
3
4
5
6
7

最近公共祖先(LCA)= 两点家谱的交汇处。朴素做法逐步爬父链 O(n);倍增法先建「跳表」up[k][u](u 往上跳 2^k 步的祖先),把一次查询压到 O(log n)。目标:LCA(7, 4) 与 LCA(6, 5)

1function lca(n: number, fa: number[], LOG: number, u: number, v: number): number {
2 const depth = new Array(n).fill(0); // 建表:depth + up[0] = fa
3 const up = [fa.slice()];
4 for (let i = 1; i < n; i++) depth[i] = depth[fa[i]] + 1;
5 for (let k = 1; k < LOG; k++) // 爸爸的爸爸
6 up.push(up[k - 1].map((p) => (p < 0 ? -1 : up[k - 1][p])));
7 if (depth[u] < depth[v]) [u, v] = [v, u];
8 for (let k = LOG - 1; k >= 0; k--) // ① 对齐:深度差按二进制拆跳
9 if ((depth[u] - depth[v]) & (1 << k)) u = up[k][u];
10 if (u === v) return u;
11 for (let k = LOG - 1; k >= 0; k--)
12 if (up[k][u] !== up[k][v]) { // ② 试跳:祖先不同才跳
13 u = up[k][u];
14 v = up[k][v];
15 }
16 return up[0][u]; // ③ 停在两孩子,父即答案
17}
树0 根;1,2 子;3,4 为 1 的子;5 为 2 的子;6 为 3 的子;7 为 6 的子
目标LCA(7, 4) 与 LCA(6, 5)
1 / 11

为什么「祖先相同不能跳」

①对齐:深度差 d 按二进制拆解,第 k 位是 1 就沿 up^k 跳一次——O(log n) 步对齐。
②试跳只认「不同」:up[k][u] = up[k][v] 说明跳完已在公共祖先上—— 但可能不是最近的那个,跳了就回不了头;不同则说明还没到交汇处,放心双跳。
③收官:从高到低试完,两点恰好停在 LCA 的两个孩子上——up⁰ 一步即答案。

建表 O(n log n)、每次查询 O(log n)。最常用的衍生是树上距离:dist(u, v) = depth[u] + depth[v] − 2·depth[LCA];离线批量查询可用 Tarjan 离线(并查集),链上重载场景还有树链剖分。与 换根 DP、树形 DP 一起,构成树上问题的三板斧。