闲谈 LCA
\(\text{LCA}\),即为最近公共祖先,是在树上的概念,是许多树上操作的必需品,如树链剖分,树上前缀和,树上差分…
那我们今天就来聊一聊求 \(\text{LCA}\) 的几种方法。
暴力
不妨从定义入手,我们可以考虑维护每个节点的 \(\text{fa}\)。
- 先让一个节点往上跳,直到与另一个节点深度相同。
- 两个节点不停的往上跳,直到一样。
下面给出一部分代码
while(dep[fa[x]] >= dep[y])
x = fa[x];
if(x == y)
return x;
while(x != y)
x = fa[x], y = fa[y];
return fa[x];
这一步看似简单,实则之后都是或多或少建立在这个基础上,所以还是需要多多注意。
倍增
上面代码的最大问题是什么?显然是跳的太慢了。因此我们可以想到用什么的方式能跳的更快并且不会跳过。
接下来我们更深入的思考,注意到我们跳上去的个数都是十进制,可以采用二进制拆分,这样既是不重不漏而且跳的速度也比一个一个跳快了许多。
这时候,或许有些人会直接上代码了,我也是。
int lca(int u, int v)
{
if (dep[u] < dep[v])
swap(u, v);
for (int i = 20; ~i; i--)
if (dep[fa[u][i]] >= dep[v])
u = fa[u][i];
if (u == v)
return u;
for (int i = 20; ~i; i--)
if (fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
不过初次看这个代码可能会有几个疑问,我找几个最显眼的。
为什么要从大到小枚举 \(i\)?
我们可以先引入一种二进制分解的方法:
从大到小枚举 \(2^i\),如果比 \(x>2^i\) 那么 \(x\) 二进制从右往左第 \(i+1\) 位一定是 \(1\),同时让 \(x=x-2^i\)。
那为什么这个方法是正确的呢?注意到当枚举到 \(2^i\) 时,\(x\) 一定小于 \(2^{i+1}\) (已经被剪掉了),如果这个时候 \(x>2^i\) 而不去减掉的话,那么就算接下来所有位数全部是 \(1\) 都无法弥补空当。因此我们确定这一位一定是 \(1\)。
那倘若反过来,就没有如此多的保障,我们不知道是否 应该 加上这一位,因此我们不能保证二进制拆分是对的。
那么这里往上跳也类似一种二进制拆分的过程,类比一下即可。
为什么最后还要 恰好 往上再走 \(1\) 步?
不妨假设在进行向上跳跃的过程中我们走到了距离 \(\text{LCA}\) 还有两格的位置,也就是说二进制拆分只能拆到这里。
那我们不妨如此考虑:
假设此时此刻往上跳跃了 \(k=2^a+2^b+2^c(a>b>c)\) 步。
而合法位置是 \(k+1=2^a+2^b+2^c+1\)。
-
若 \(c\ne 0\),此时 \(2^a+2^b+2^c+2^0\) 无疑是最佳选择,因此不可能。
-
若 \(c=0\),此时考虑进位,直到满足二进制逻辑即可,同样不可能。
因此无论如何都只会差一格。至于其他的距离类比一下也可以。
树链剖分
首先我们来回顾一下树链剖分当中的一些定义。
定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
定义 轻子节点 表示剩余的所有子结点。
从这个结点到重子节点的边为 重边。
到其他轻子节点的边为 轻边。
若干条首尾衔接的重边构成 重链。
(定义摘自 OIWIKI )
其实我们是运用到了几个性质:
- 每个节点都属于只属于一条重链。
- 每条路径都可以被拆分成不超过 \(O(\log n)\) 条重链,因为每走一条轻边所在的子树大小至少会除以 \(2\)。
那么如何利用这个计算 \(\text{LCA}\)?这里先给出过程。
int lca(int u, int v)
{
while (top[u] != top[v])
{
if (dep[top[u]] < dep[top[v]])
swap(u, v);
u = fa[top[u]];
}
return dep[u] < dep[v] ? u : v;
}
此处 \(top_u\) 表示 \(u\) 所在的重链顶点。
每次都让重链深的往上跳重链,直到跳到同样的重链,此时深度浅的便是 \(\text{LCA}\)。
至于为什么是这样的,我们不妨如此思考:
首先,位于同一条重链,此时,深度浅的便是深度大的祖宗,保证了是两者的公共祖先,而且因为是第一次遇到相同重链,所以一定是最近的 (细想一下,此时 \(v\) 第一次跳到了 \(u\) 祖先的位置)。
依据性质,保证查询复杂度是 \(O(\log n)\) 的。
欧拉序
引入欧拉序的概念:
从根结点出发,按 \(\text{dfs}\) 的顺序在绕回原点所经过所有点的顺序。
对于 \(x\),\(y\)。它们的 \(\text{LCA}\) 即为第一次出现的 \(x, y\) 欧拉序之间深度最小的点。
为什么是第 \(1\) 次?
其实这个不重要,第一次和最后一次之间的都是儿子,所以不会有什么区别。
为什么是之间的?
首先根据定义,我们知道,欧拉序在 \(x,y\) 之间的一定是 \(x, y\) 的公共祖先,因为不是是 \(x\) 回溯往前到达某个点然后进入 \(y\),就是 \(x\) 往下搜索到 \(y\),它们之间一定会有最近公共祖先。
为什么是深度最浅的?
如果是次浅的,那么此时就不是公共祖先,只是其中某一方的祖先。至于为什么最近,其实很好理解,因为这一段就是 \(x\) 回溯往上到 \(\text{LCA}\) 再下去刚好碰到 \(y\)。这个模拟一个样例即可,一共就两种情况。
代码如下:
int LCA(int u, int v)
{
if (dfn[u] > dfn[v])
swap(u, v);
u = dfn[u], v = dfn[v];
int K = Log[v - u + 1];
int fa1 = f[u][K], fa2 = f[v - (1 << K) + 1][K];
if (dep[fa1] < dep[fa2])
return fa1;
else
return fa2;
}
(此算法必须感谢 @Rainbow_qwq 神仙)。复杂度 \(O(1)\)。
\(\text{END.}\)

浙公网安备 33010602011771号