算法 | 最近公共祖先
最近公共祖先 (LCA, Lowest Common Ancestor):树中两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
朴素算法
预处理
记录树中每个节点 $i$ 的深度 $dep_i$ 和父节点 $fa_i$。可以在 DFS 遍历整棵树的同时完成这个预处理。
时间复杂度:$O(V)$
求解
给出要求解 LCA 的两个节点 $a,b$,不妨设 $dep_a\geq dep_b$.
首先,较深的节点 $a$ 需要先向上跳到和 $b$ 一样的深度:
while (dep[a] > dep[b])
a = fa[a];
跳到一样的深度后,如果 $a=b$,则说明二者的 LCA 就是 $b$,输出 $b$ 结束算法。
如果 $a\neq b$,则说明二者的 LCA 还在上层,接下来 $a$ 和 $b$ 一起往上一层层跳,直到找到公共祖先。
while (a != b)
a = fa[a], b = fa[b];
时间复杂度:$O(d)$,其中 $d$ 为树的深度。对于一个随机树,它的平均深度 $d=\log V$,那么时间复杂度为 $O(\log V)$.
倍增算法
上面朴素算法最大的问题就是跳得太慢了,每次都是一层一层得往上找。我们其实可以用倍增来找,即每次向上跳 $2^n$ 层。
以下面这个树作为例子,要求 $14$ 和 $15$ 的 LCA:
1 | 1
| / \
2 | 2 3
| /|\ / \
3 | 5 6 7 8 9
| / \
4 | 10 11
| | |
5 | 12 13
| | |
6 | 14 15
- 找 $14$ 和 $15$ 往上 $8$ 层的祖先,发现二者祖先已经超出根节点了,我们看作相同,缩小距离继续。
- 找 $14$ 和 $15$ 往上 $4$ 层的祖先,发现二者都为 $2$,祖先相同,缩小距离继续。
- 找 $14$ 和 $15$ 往上 $2$ 层的祖先,发现二者为 $10,11$,祖先不同,向上跳后继续。
- 找 $10$ 和 $11$ 往上 $1$ 层的祖先,发现二者都为 $5$,祖先相同,缩小距离继续。
- 距离缩小到 $0$,结束算法。此时 $a$ 或 $b$ 的上一层祖先便是求出的 LCA.
有一点比较难理解,为啥祖先不同时往上跳,祖先相同时只缩小距离。其实这有点二分的味道:
如下图,假设 $a,b$ 两点的 LCA 为 $11$,那么首先跳 $16$ 发现祖先相同,于是下一步跳 $8$ 发现祖先不同...... 其实就是二分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
=============================================√
=====================x
===========√
=====x
==√
当然,倍增算法首先也要把两个点先跳到一层,跳的方法也是使用倍增来完成。
例如 $a$ 比 $b$ 深 $7$ 层的话,就会跳 $4+2+1$,最后 $a,b$ 到同一层。
预处理
记录树中每个节点 $i$ 的深度 $dep_i$ 和向上跳 $2^j$ 层的父节点 $fa(i,j)$。可以在 DFS 遍历整棵树的同时完成这个预处理。
父节点的维护其实算作一种动态规划,转移方程为:$fa(i,j+1)=fa(fa(i,j),j)$
另外,可以预处理出来所有的 $\lfloor \log_2 x\rfloor+1$,这个是一个常数级别优化。递推方式为:mylog2[i] = mylog2[i - 1] + (1 << mylog2[i - 1] == i);
constexpr int MAXN = 1e6 + 10;
int h[MAXN], e[MAXN], ne[MAXN], idx;
int mylog2[MAXN]; // \lfloor log_{2}{x} \rfloor + 1
int fa[MAXN][30], dep[MAXN];
void init()
{
memset(h, -1, sizeof(h));
for (int i = 1; i < MAXN; i++)
mylog2[i] = mylog2[i - 1] + (1 << mylog2[i - 1] == i);
}
void dfs(int now, int father)
{
fa[now][0] = father;
dep[now] = dep[father] + 1;
for (int i = 0; i < mylog2[dep[now]]; i++)
fa[now][i + 1] = fa[fa[now][i]][i];
for (int i = h[now]; i != -1; i = ne[i])
if (e[i] != father)
dfs(e[i], now);
}
时间复杂度:$O(V\log d)$,其中 $d$ 为树的深度。
计算
int lca(int a, int b)
{
if (dep[a] < dep[b])
swap(a, b); // make a >= b
while (dep[a] > dep[b])
a = fa[a][mylog2[dep[a] - dep[b]] - 1];
if (a == b)
return a;
for (int i = mylog2[dep[a]] - 1; i >= 0; i--)
{
if (fa[a][i] != fa[b][i])
{
a = fa[a][i];
b = fa[b][i];
}
}
return fa[a][0];
}
时间复杂度:$O(\log d)$,其中 $d$ 为树的深度。
树链剖分
预处理时间复杂度:$O(n)$
查询时间复杂度:$O(\log n)$
具体介绍见:树链剖分
本文采用 CC BY-SA 4.0 许可,本文 Markdown 源码:Haotian-BiJi