浅谈——最近公共祖先

变量定义

\(u,v\) 是有根树中任意两个不相等的点。\(root\) 是该有根树的根。\(\text{dep}_u\) 是以 \(root\) 为根的有根树中 \(u\) 的深度。

不妨设 \(\text{dep}_u>\text{dep}_v\)

概念定义

最近公共祖先英文全称 \(\tt{Lowest Common Ancestor}\),简称 \(\text{LCA}\)。该问题匹配有根树,对于 \(u\)\(v\) 两节点的最近公共祖先记为 \(\text{LCA}(u,v)\),表示 \(u,v\) 公共祖先内最近的那个。

换句话说,如果给定根 \(root\),则 \(\text{LCA}\) 可以感性的理解:

  • \(v\)\(u\) 的祖先,则 \(\text{LCA}(u,v)=v\)
  • \(v\) 不是 \(u\) 的祖先,则显然路径 \(root\rightarrow u\) 和路径 \(root \rightarrow v\) 一定会有一段重合,即路径 \(root\rightarrow \text{LCA}(u,v)\)

step1.朴素法

考虑 \(O(n)\sim O(n)\) 求解。

  • \(u\) 一步一步朝父亲跳,直到深度与 \(v\) 一样。
  • \(u\)\(v\) 一起朝各自的父亲跳,直到重合。

此时的 \(u\)\(v\) 就是原来两点的 \(\text{LCA}\)

预处理父亲 \(O(n)\),每次查询最坏 \(O(n)\)

step2.倍增法

考虑同样的思路。发现瓶颈在每次跳的时候直跳一步耗时间。考虑将一共跳的步数用二进制拆分。

具体来说,定义 \(f_{i,j}\) 表示从点 \(i\)\(root\)\(2^j\) 步的祖先。

显然有转移 \(f_{i,j}=f_{f_{i,j-1},j-1}\),表示先跳 \(2^{j-1}\) 步再跳 \(2^{j-1}\) 步。

那么预处理就是 \(O(n \log n)\) 的。

此时原先第一步跳到一样的高度就可以 \(\log n\) 解决了。考虑 \(j\)\(\log n\)\(0\) 枚举,因为如果能大跳就不用小跳浪费时间。判断当前 \(u\) 向上跳 \(2^j\) 的深度是比 \(v\) 大还是小。大则跳,小则不跳。

然后就是同深度的了。同样考虑上述过程,如果能大跳就大跳,不能则小跳。能跳的情况就是 \(u\)\(v\) 跳完后仍然不是同一点。

那么最后的答案就是 \(f_{u,0}\)\(f_{v,0}\)

单次查询就是 \(O(\log n)\) 的。

step3.树链剖分

此方法可以得到 \(\text{LCA}(u,v)\) 就是两点跳到同一条重链时深度小的那个点。

预处理深度和重链是 \(O(n)\),单次查询也是 \(O(\log n)\)

step4.\(\text{Tarjan}\)

这是一个离线算法。

离线下询问,并使用并查集。然后采用如下步骤:

  • 标记当前节点 \(u\) 已访问,但是还没有回溯。
  • 枚举所有没有访问的过的子节点 \(v\),继续遍历子节点。
  • \(v\) 合并到 \(u\) 上。
  • 访问所有与当前节点有询问并且回溯过的 \(v\),则 \(\text{LCA(u,v)}\) 就是 \(v\) 在并查集中的祖先。
  • 标记当前节点回溯。

初始化并查集 \(O(n)\),由于离线,总共 \(q\) 次询问的时间复杂度是 \(O((n+q)\alpha(n))\)\(\alpha(n)\) 极小,是路径压缩后并查集的复杂度,可以当成常数。所以询问就是 \(O(n+q)\) 的。

step5.\(\text{dfs}\)

\(O(n)\) 搞定时间戳 \(dfn_u\)\(\text{dfs}\) 序。

考虑 \(\text{dfs}\) 序的性质:祖先先于后代被遍历,即若 \(u\)\(v\) 的祖先,\(dfn_u<dfn_v\)。以下记 \(\text{LCA}(u,v)\)\(d\)

\(dfn_u<dfn_v\)(这里不遵循开头的设法)。

此时有两种情况。就是 \(u,v\) 构不构成祖孙关系。

  • 如果不构成祖孙关系。\(\text{dfs}\) 序的顺序就是 \(d \rightarrow u \rightarrow d \rightarrow v\)。显然 \(\text{dfs}\) 序在 \(u,v\) 之间的点都属于以 \(d\) 为根的子树。所以 \(d\rightarrow v\) 路径上 \(d\) 的儿子 \(v^\prime\),找到这个 \(v^\prime\)(或者对应的 \(u^\prime\)),也就是深度最小的,它的父亲就是 \(d\)
  • 如果构成祖孙关系。那么 \(d\) 就是 \(u\)

可惜的是,我们要求深度最小的点,考虑 \(\text{ST}\) 表。所以预处理 \(O(n\log n)\),每次查询 \(O(1)\)

貌似优秀一点的解法可以不用 \(\text{ST}\) 表?具体请去浅谈——RMQ 问题。所以是可以 \(O(n)\sim O(1)\)

step6.欧拉序

似乎类似 \(\text{dfs}\) 序,但是难写。部分题目需要同时记录欧拉序和 \(\text{dfs}\) 序。欧拉序也就是遍历顺序,节点可以重复。思路与 \(\text{dfs}\) 序相同。不讲了。

整理如下

方法 时间复杂度 优缺点
朴素法 \(O(n)\sim O(nq)\) 思路简单,但效率低
倍增法 \(O(n\log n) \sim O(q\log n)\) 思路简单,效率较高,能通过大部分题
树链剖分 \(O(n)\sim O(q\log n)\) 预处理复杂度低,常数小,但难写
\(\text{Tarjan}\) \(O(n)\sim O(n+q)\) 离线算法,复杂度优秀,但不常用
\(\text{dfs}\) \(O(n\log n)\sim O(q)\\O(n)\sim O(1)\) 复杂度超级优秀,适合 \(q\) 极大的题目
欧拉序 \(O(n\log n)\sim O(q)\\O(n)\sim O(1)\) 与上个算法思路一样,但更麻烦(大众)

习题就不放了。

posted @ 2026-05-23 09:55  三线八角  阅读(11)  评论(0)    收藏  举报