最近公共祖先(LCA问题)
最近公共祖先(LCA问题)
板子题:
例题:1172.祖孙询问
次小生成树应用:
例题:1171. 距离
树上差分应用:
例题:352. 闇の連鎖
三种方法如下:第二和第三种更实用一些
法一:向上标记法
时间复杂度:
- 每次查询:最坏情况 O(n)
算法实现:
- x 向上走到根节点,并标记所有经过的节点
- y 向上走到根节点,当第一次遇到已经标记的节点时,就找到了 LCA(x, y)。
法二:树上倍增法
时间复杂度:
- 预处理:O(nlogn)
- 每次查询:O(logn)
算法实现:
- 预处理:
- 预处理数组 fa[i][j] :表示从 i 开始,向上走 2^j 所能走到的节点。0 <= j <= logn。
- 如何预处理:dfs 或 bfs 都可以
- 若fa[i][j]的节点不存在,则令
fa[i][j] == 0
。 - 当
j == 0
时,fa[i][j] = i 的父节点 - 当
j > 0
时,fa[i][j] = fa[fa[i][j - 1]][j - 1],即、先跳到2^(j-1) 步,再跳 2^(j-1) 步
- 预处理数组 depth[i]:表示深度,或层数,规定根节点的深度为 1,子节点的深度为父节点深度 +1
- 预处理数组 fa[i][j] :表示从 i 开始,向上走 2^j 所能走到的节点。0 <= j <= logn。
- 具体步骤如下:基于倍增的思想
- 先将两个点跳到同一层,
- 让两个点同时向上跳,一直跳都他们的最近公共祖先的下一层(因为,当二者相等时无法判断是否为最近的公共祖先,所以跳到最近公共祖先的下一层)
- 哨兵:depth[0] = 0。如果从 i 开始跳 2^j 步会跳过根节点,那么 fa[i][j] = 0,此时depth[fa[i][j]] = 0,哨兵生效,停止跳跃。
- 具体作用如下:
- 保证 lca 两点跳到同一层时,越界的深度小于任何树中节点的深度,从而停止此次跳跃;
- 保证 lca 查找公共祖先时,越界的二者的父节点相同,都是0,从而停止此次跳跃。
具体板子如下:
1 |
|
法三:Tarjan算法——离线求LCA
介绍:
算法本质是,使用并查集对向上标记法的优化。离线算法,读入所有询问,统一计算,统一输出
时间复杂度
- O(n + m)
算法实现
- 在深度遍历时,将所有点分为三大类
- 第一类点:已经遍历过,并且已经回溯过的点,标记为 2
- 第二类点:正在搜索的分支,标记为 1
- 第三类点:还未搜索到的点,标记为 0
- 若求lca(x, y),对于正在访问的节点 x ,即、x 的标记为 1。若 y 是已经遍历过且回溯过的节点,则 lca(x, y) 就是从 y 向上走到根,第一个遇见的标记为 1 的节点,其实就是向上标记法。
- 并查集优化:
- 当一个节点获得标记 2 时,把它所在的集合合并到其父节点所在的集合中(合并时,其父节点一定为 1)
- 所有标记为 2 的节点,都有一个指针指向其祖宗节点。若 x 的标记为 1 ,y 的标记为 2,查询 y 的向上走的第一个标记为 1 的节点就是 y 的祖宗节点(一定标记为 1 ),此祖宗节点就是 lca(x, y)
具体板子如下
1 |
|
法四:dfs序列 + RMQ算法(例如,st表)
麻烦,且很不常用,了解即可
算法思路:
- dfs 遍历得 dfs 序列
- 若求 lca(x, y) ,则在 dfs 序列中,找任意的 x 和 y 求之间的最小值,此最小值就是 lca(x, y)
LCA 应用
LCA 树上倍增算法的应用 —— 次小生成树
将朴素求树上两点路径之间的最值,化为倍增求最值,从而将时间复杂度由 O(n^2) 优化为 O(nlogn)
1 |
|
LCA 应用 —— 树上差分
点的权值表示边的权值,其中 d[i]:表示差分节点 i 的权值(即、i 通向父节点的边的权值);如果在 x, y的路径上 + c,则只需令 d[x] += c, d[y] += c, d[p] -= 2c;(其中 p = lca(x, y))。最终答案为d[res]:res 的所有子树的权值和
代码如下:
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Moench 的算法宝库!