LCA 问题
模板题
最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 ——oi wiki
朴素算法
过程:
每次找深度比较大的那个点,让它向上跳。这两个点最后一定会相遇,相遇的位置就是想要求的 LCA。
单次查询的时间复杂度为 O(n),一般不被使用。(故省略代码
倍增算法
该算法是朴素算法的改进算法,也是最经典的 LCA 算法,本质上是增加朴素算法中跳跃的距离以减少操作次数。
时间复杂度:预处理 O(nlogn),单次查询 O(logn)。
思路:
预处理:
- 利用 DFS 遍历树,记录每个节点的深度和每个节点的 2^k^级祖先。
- 对于每个节点 u,记录它的 2^k^级祖先,k 从 0 到 $log_2N$.
查询:
对于每次查询的两个节点 a 和 b
- 将深度较大的节点向上跳到与另一个节点相同的深度。
- 向上跳 2^k^步,直到找到 LCA.
核心思想:
- 利用二进制分解的思想,将跳跃步长分解为 2^k^ 的幂次。
- 例如,如果需要跳 5 步,可以分解为 2^0^+2^2^(即跳 1 步 + 跳 4 步)。
- 通过预处理每个节点的 2^k^ 级祖先,可以快速实现任意步长的跳跃。
具体步骤:
- 调整深度
- 如果两节点深度不同,将深度较大的节点跳到另一个节点相同的深度
- 同时上跳
- 两个节点深度相同时,同时上跳 2^k^ 步,知道找到 LCA。
- 跳跃时,如果跳 2^k^ 步后两个节点的祖先不同,则跳;否则不跳。
- 最终,两个节点会跳到 LCA 的直接子节点,此时它们的父节点就是 LCA。
代码:
相关变量
1 | const int Log = 20; |
预处理部分
- 计算深度:
depth[u] = depth[fa] + 1当前节点u的深度是其父节点parent的深度加 1。- 根节点深度为 0.
- 2^k^级祖先:
up[u][0] = fa当前节点u的 2^0^ 父节点是fa。up[u][k] = up[up[u][k - 1]][k - 1]当前节点u的 2^k^ 级祖先可以通过其 2^k-1^级祖先的 2^k-1^ 级祖先得到。
- 递归遍历子树:
- 对于当前节点
u的每一个子节点v,如果v不是父节点fa,则递归调用dfs(v, u)。
- 对于当前节点
1 | void dfs(int u, int fa){ |
查询部分
调整深度:
- 如果
depth[a] < depth[b],交换a和b,确保a是深度较大的节点。 - 计算深度差
diff = depth[a] - depth[b]。 - 通过
diff的二进制表示,将a跳到与b相同的深度。
- 如果
检查是否找到:
- 如果
a和b已经相等,说明a(或b)就是 LCA,直接返回。
- 如果
同步上跳:
- 从最大的 k 开始(
Log - 1到0),尝试将a和b同时向上跳 2^k^ 步。 - 如果
up[a][k] != up[b][k],说明跳 2^k^ 步后不会跳过 LCA,因此可以安全地跳。 - 最终,
a和b会跳到 LCA 的直接子节点,此时up[a][0]就是 LCA。
- 从最大的 k 开始(
1 | int lca(int a, int b){ |
主函数
1 | int main() |