闲谈 LCA

\(\text{LCA}\),即为最近公共祖先,是在树上的概念,是许多树上操作的必需品,如树链剖分,树上前缀和,树上差分…

那我们今天就来聊一聊求 \(\text{LCA}\) 的几种方法。

暴力

不妨从定义入手,我们可以考虑维护每个节点的 \(\text{fa}\)

  1. 先让一个节点往上跳,直到与另一个节点深度相同。
  2. 两个节点不停的往上跳,直到一样。

下面给出一部分代码

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\)

  1. \(c\ne 0\),此时 \(2^a+2^b+2^c+2^0\) 无疑是最佳选择,因此不可能。

  2. \(c=0\),此时考虑进位,直到满足二进制逻辑即可,同样不可能。

因此无论如何都只会差一格。至于其他的距离类比一下也可以。

树链剖分

首先我们来回顾一下树链剖分当中的一些定义。

定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。

定义 轻子节点 表示剩余的所有子结点。

从这个结点到重子节点的边为 重边

到其他轻子节点的边为 轻边

若干条首尾衔接的重边构成 重链

(定义摘自 OIWIKI )

其实我们是运用到了几个性质:

  1. 每个节点都属于只属于一条重链。
  2. 每条路径都可以被拆分成不超过 \(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.}\)

posted @ 2025-08-23 16:36  ljfyyds  阅读(25)  评论(0)    收藏  举报