LCA问题:给定一颗 个结点的有根树,有 次询问,每次询问给出结点 ,求 的最近公共祖先。此类问题通称 LCA 。


暴力

一次一次把 向上提,知道 相等为止。

倍增加速

在上面暴力的思想中,是一步一步向上跳的。

倍增的核心思想就是 步向上跳。

我们需要先处理出第 个点的第 个祖先。

如何求出?

为第 个点向上跳 后所处的位置,根据初一学习的幂的性质,我们认为第 个点向上跳 后所处的位置就是第 个点向上跳 后再向上跳 )。

所以我们有:

可以递推出来:

void dfs(int u, int fa) //当前是第 u 个结点,u 的父亲是 fa
{
    d[u] = d[fa]+1;  /顺便计算深度
    for(int i=1; (1<<i) <= d[u]; i++) //(1<<i)就是 2^i ,枚举2的次幂不能超过当前结点的深度
        f[u][i] = f[ f[u][i-1] ][i-1];

    for(int i=head[u]; i; i=nxt[i])
    {
        int v = ver[i];
        if(v == fa) continue;
        f[v][0] = u;
        dfs(v, u);
    }
}

我们有了 ,可以干啥呢?

假设要求

我们需要先把 弄成同一深度,方便待会“一起跳”。

那么为了把 弄成同一深度,显然需要把那个深度更深的结点不断向上跳,直到深度等于另一个结点为止。

为了避免麻烦,我们先让 变成那个深度更大的结点即可。不影响最终结果。

我们有代码:

if(d[a]<d[b]) swap(a, b);

for(int i=20; i>=0; i--) //2^20足够了。想要省事可以用 log(a) 优化
{
    if(d[f[a][i]] >= d[b]) a = f[a][i];
    //把 a 不断向上跳
    if(a == b) return a;
    //特判。如果两个点相等那么 LCA(a, b) 就是深度小的那个点
}

我们考虑从一个较大的数 作为 的次幂枚举,如果 两点的第 个祖先(即 )不一样,那么就

也就是 同时向上跳 步。

这样,在从大到小枚举所有的 之后,当前的 必定是 的左右儿子,那么返回一个 即可(即 的父亲)。

注意!这里说的都是**从大到小枚举 的次幂**,一定要记住,否则这个算法就错了。

我们有求 的代码:

int LCA(int a, int b)
{
    if(d[a]<d[b]) swap(a, b);

    for(int i=20; i>=0; i--)
    {
        if(d[f[a][i]] >= d[b]) a = f[a][i];
        if(a == b) return a;
    }

    for(int i=20; i>=0; i--)
        if(f[a][i] != f[b][i])
            a = f[a][i], b = f[b][i];

    return f[a][0];
    //此处亦可返回 f[b][0] ,答案都一样
}

是不是很简单呢。

考虑为什么倍增 LCA 不会超时。

就像上面说的一样,暴力做法是一步一步往上跳,相当于每次只跳 步。

而倍增 LCA 通过从大到小枚举 的次幂,在枚举到每个 时一下会跳 次,在倍增中只跳了一次,而在暴力中要跳 次!差异可想而知。

接下来谈谈为什么从 开始枚举。

如果您多尝试几次,就会发现其实一般数据根本不会用到 之前的值!

为什么呢?看下面:

来观察 的函数图像:

GrUEin.png

发现了什么?函数 的增长速度是指数级的!

那么为什么说数据根本不会用到 之前的值呢?

而在洛谷的 LCA 模板题中,点数 的最大值才为

一般来说,我们可以让 开始枚举。这样会更有效。

顺带一提,由于我们求出 数组需要耗费 的复杂度,而真正求 LCA 只需要 的时间复杂度,故单次查询时间复杂度为

考虑为什么倍增 LCA 的正确性。

在倍增 LCA 的算法中,我们发现对于 必须满足一个条件:可以拆成任意个 的次幂相加的形式,否则就无法倍增的跳到答案的左、右儿子。

例如说:

故对于编号为 的点一条可行的路径为:

由于百度的原因,我们知道了任意一个正整数都可以拆分成 的次幂的形式。故倍增 LCA 是必定正确的。

考虑为什么从大到小枚举 的次幂。

其实很简单。如果从小到大枚举,那么几乎小于 (所要求 LCA 的一个值)的任意一个 的次幂都会计入答案。那么是不是就意味着会出现“跳不到答案的左右儿子”这个情况(因为你先跳小的,那么有可能到某个节点(不是答案结点的左右儿子)时 的越来越大的次幂就无法跳了)。

为了避免这种情况,我们就需要从大向小枚举。这样,不能跳的也不会跳。仔细想一想似乎也很符合倍增 LCA 的思想:一次跳很多步。