浅谈——最近公共祖先
变量定义
\(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)\) | 与上个算法思路一样,但更麻烦(大众) |
习题就不放了。

浙公网安备 33010602011771号