算法 | 最近公共祖先
最近公共祖先 (LCA, Lowest Common Ancestor):树中两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
朴素算法
预处理
记录树中每个节点
时间复杂度:
求解
给出要求解 LCA 的两个节点
首先,较深的节点
while (dep[a] > dep[b])
a = fa[a];
跳到一样的深度后,如果
如果
while (a != b)
a = fa[a], b = fa[b];
时间复杂度:
倍增算法
上面朴素算法最大的问题就是跳得太慢了,每次都是一层一层得往上找。我们其实可以用倍增来找,即每次向上跳
以下面这个树作为例子,要求
1 | 1
| / \
2 | 2 3
| /|\ / \
3 | 5 6 7 8 9
| / \
4 | 10 11
| | |
5 | 12 13
| | |
6 | 14 15
- 找
和 往上 层的祖先,发现二者祖先已经超出根节点了,我们看作相同,缩小距离继续。 - 找
和 往上 层的祖先,发现二者都为 ,祖先相同,缩小距离继续。 - 找
和 往上 层的祖先,发现二者为 ,祖先不同,向上跳后继续。 - 找
和 往上 层的祖先,发现二者都为 ,祖先相同,缩小距离继续。 - 距离缩小到
,结束算法。此时 或 的上一层祖先便是求出的 LCA.
有一点比较难理解,为啥祖先不同时往上跳,祖先相同时只缩小距离。其实这有点二分的味道:
如下图,假设
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
=============================================√
=====================x
===========√
=====x
==√
当然,倍增算法首先也要把两个点先跳到一层,跳的方法也是使用倍增来完成。
例如
预处理
记录树中每个节点
父节点的维护其实算作一种动态规划,转移方程为:
另外,可以预处理出来所有的 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);
}
时间复杂度:
计算
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];
}
时间复杂度:
树链剖分
预处理时间复杂度:
查询时间复杂度:
具体介绍见:树链剖分
本文采用 CC BY-SA 4.0 许可,本文 Markdown 源码:Haotian-BiJi