父亲和祖先
讲 最近公共祖先(Least Common Ancestors,LCA) 之前,先回到有根树的一些概念。
假设这棵树以 0 为根,那么就有 00 是 2,4 的父亲,44 是 1,5 的父亲,2 是 3,6 的父亲。
接下来说一下 祖先 这个概念。对于一个结点 xx,它自己,它的父亲,它的父亲的父亲 ⋯ 都是它的祖先。换句话来讲,xx 到根的最短路径上面的所有结点都是它的祖先。例如对于结点 1,它有三个祖先,分别是 1,4,0。
那么接下来我们来看最近公共祖先。对于树上的两个结点 x,y,它们都会有到根的一条最短路径。这两条路径必然有重复的点(因为都会到达根),而在这些重复的点当中,深度最大的点,就是 x,yx,y 的最近公共祖先。例如,图上 1,5 两个点的最近公共祖先是 4,而 1,3 两个点的最近公共祖先是 00。
如果是一棵随机生成的均匀的树,刚才的算法的时间耗费实际上已经足够我们去在很短的时间内求解一组点的最近公共祖先。但是如果这棵树比较极端(比如是一条链),那么直接按照 LCA 的定义去求解就会变得非常慢,一般而言都无法满足题目给出的时间要求。
求 LCA 最容易想到的方案是:
- 先从 x 往上走到根,沿途会经过 x 所有的祖先,把它们用一个数组标记。
- 再从 y 往上走到根,沿途会经过 y 所有的祖先,遇到的第一个被标记的点就是 x, 的最近公共祖先。
代码如下,时间复杂度为 O(n)。
int fa[MAX_N], vis[MAX_N]; // fa 数组保存每个结点的父节点,vis 数组用来标记
int LCA(int x, int y) {
memset(vis, 0, sizeof vis);
while (x != 0) {
vis[x] = 1;
x = fa[x];
}
while (vis[y] == 0) {
y = fa[y];
}
return y;
}
标记所有的祖先似乎太浪费了,一种更好的想法是:先让 x,y 走到同一深度,然后一起往上走,第一个相遇的位置就是它们的 LCA。
那么需要先通过 DFS 求出每个结点的深度,代码如下:
int d[MAX_N], fa[MAX_N]; // d 数组保存每个结点的深度
void dfs(int u) {
d[u] = d[fa[u]] + 1;
for (int i = p[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v != fa[u]) {
fa[v] = u;
dfs(v);
}
}
}
int lca(int x, int y) {
if (d[x] < d[y]) {
swap(x, y); // 让 x 为深度更深的那个点
}
while (d[x] > d[y]) {
x = fa[x]; // 让 x 和 y 处于同一深度
}
while (x != y) {
x = fa[x];
y = fa[y];
}
return x;
}
但这种做法的时间复杂度依然为 O(n)。
瓶颈在于通过 fa 数组往上走,每次走一步实在太慢了。那么有没有方法可以一次性走一大步呢?
答案是采用二进制的思想尝试往上跳,以下面这段代码为例:
while (d[x] > d[y]) {
x = fa[x]; // 让 x 和 y 处于同一深度
}
可以改为:
int K = 0;
while ((1 << (K + 1)) <= d[x]) {
K++;
}
for (int i = K; i >= 0; i--) {
//如果 x 的 2^i 祖先深度大于等于 y 的深度,x 就往上跳到 2^i 祖先
}
其中 K 为最大的整数满足 2 ^ K≤d[x]。
我们让 x 每次尝试跳 2^i 步,i 从 K 开始从大到小枚举。如果跳跃后深度依然不小于 y,就选择跳跃。
换种角度思考,设 t=d[x]-d[y],那么 t 的二进制表示中 1 的位置就是 x 要跳的那步。相当于用若干个不同的 2 的幂次来凑出这个 t,我们肯定会选择从大到小凑,并且最终方案肯定是唯一的。
同理,当 x,y 到达同一深度后,两个点继续同时往上跳的步骤也可以用这种二进制尝试跳跃的方法。
如果能在 O(1) 时间内得到个结点的 2 的幂次辈祖先,那么这种方法计算 LCA(x,y) 的时间复杂度就为 O(logn)。
现在的问题变为如何预处理每个结点的 2 的幂次辈祖先?
解决方法是采用动态规划,定义f[u][j]
表示u
结点 2^j 辈祖先(如果不存在就为 0)。那么f[u][0]
就是u
结点的父亲结点,在 DFS 求深度的时候同时维护一下即可。
int f[MAX_N][20], d[MAX_N];
void dfs(int u) {
d[u] = d[f[u][0]] + 1;
for (int i = p[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v == f[u][0]) {
continue;
}
f[v][0] = u;
dfs(v);
}
}
然后通过递推计算所有结点的 2 的幂次辈祖先:
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i <= n; i++) {
f[i][j] = f[f[i][j - 1]][j - 1];
}
}
转移过程也很好理解,i 的 2^j 辈祖先等于 i 的 2 ^ j−1 辈祖先的 2 ^ j−1 辈祖先。
这步预处理的时间是复杂度为 O(nlogn)。
然后我们用刚才说的方法求 LCA:
int lca(int x, int y) { if (d[x] < d[y]) { // 让 x 是较深的点 swap(x, y); } int K = 0; while ((1 << (K + 1)) <= d[x]) { // 找到不超过 x 深度的最大的 2 ^ k K++; } for (int j = K; j >= 0; j--) { // 尝试让 x 往上跳,跳到与 y 到同一高度 if (d[f[x][j]] >= d[y]) { x = f[x][j]; } } if (x == y) { // 如果这个时候两个点相等,那说明原来 y 是 x 的某个祖先,直接返回当前这个点就可以了 return x; } for (int j = K; j >= 0; j--) { // 同时往上跳,跳到尽量高,但要求跳到的点还是不同的 if (f[x][j] != f[y][j]) { x = f[x][j]; y = f[y][j]; } } return f[x][0]; // 最后 x 和 y 的父节点就是它们的 LCA 了 }