LCA倍增解法

LCA今天看了一下 ,就是给你一颗树,让你求出两个点的最近公共祖先。如果对图遍历比较熟悉的话,感觉还是挺简单的。

先讲讲暴力思路吧,懂了暴力这个再用倍增优化会比较好理解。

暴力思路,首先维护两个数组:
1.depth[vertice]表示vertice的深度
2.father[vertice]表示vertice的上一个祖先
既然是最近公共祖先,那么我们先把两个点u、v的深度处理到一致,然后两个点一起往根遍历,直到u=v,此时u(v)就是开始时u、v的最近公共祖先。

洛谷LCA板子题

暴力代码:

ll depth[500010];
ll head[500010];
ll father[500010];
ll ct = 1;

struct node
{
    ll v, next;
} e[1000010];

void add(ll u, ll v) //建图
{
    e[ct].v = v;
    e[ct].next = head[u];
    head[u] = ct++;
}

void dfs(ll now, ll fa, ll dep) //dfs处理depth和father
{
    depth[now] = dep;
    father[now] = fa;
    for (int i = head[now]; i; i = e[i].next)
    {
        if (e[i].v != fa)
            dfs(e[i].v, now, dep + 1);
    }
}

ll lca(ll u, ll v)
{
    while (depth[u] > depth[v])
        u = father[u];
    while (depth[v] > depth[u]) //将uv的深度处理到一致
        v = father[v];

    while (v != u)
    {
        v = father[v];
        u = father[u];
    }
    return u;
}

int main()
{
    ll n, m, s;
    cin >> n >> m >> s;
    for (int i = 1; i < n; i++)
    {
        ll u, v;
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }
    dfs(s, 0, 1);

    for (int i = 1; i <= m; i++)
    {
        ll u, v;
        cin >> u >> v;
        cout << lca(u, v) << endl;
    }

    return 0;
}

这样写单次询问的时间复杂度为 O n On On m m m次询问就是 m n mn mn,显然会:
在这里插入图片描述

接下来就用倍增优化。其实倍增优化很简单,就是一次尽可能往上多跳几次,在这里用2的倍数进行优化,也就是很常见的二进制优化。例如公共祖先要往上10层,我们就可以第一次直接跳跃8层,第二次直接跳跃2层完成(虽然看完下面你会知道实际上第二次直跳一层)。

同样,我们要处理depth数组,但是father数组我们增加到二维,father[i][j]表示顶点i往上的第 2 ( j − 1 ) 2^{(j-1)} 2(j1)个祖先

例如上面的例题样例解释中,father[3][1]和father[5][1]都表示顶点4。father[3][0]和father[5][0]表示顶点1。

怎么样快速处理出完整的father数组呢?

我们可以递推。
在暴力写法中,我们已经知道可以在dfs中求出father[i][0]也就是顶点i的上一个祖先了。事实上,我们可以用father[i][0]递推出顶点i的所有需要的father数组信息。

看上图,dfs是从根结点开始搜索的,当我们处理father[3]时,4、2、1的father信息都已经知道了。

father[3][0]也已经在dfs到顶点3的时候就知道了,我们要做的就是递推出father[3][1]、father[3][2]、…father[3][n](当然,这题只需要处理到father[3][1])。

直接给出转移方程:father[i][j]=father[father[i][j-1]][j-1]

其实很好理解,自己想想就知道了,例如x的第2个祖先的第2个祖先是x的第4个祖先、x的第8个祖先的第8个祖先是x的第16个祖先。

另外有个细节问题:其实我们跳跃的最终目标不是最近公共祖先上,而是这个最近公共祖先的子节点,因为我们从大往小跳跃(比如要跳10步,我们先跳8再跳2),如果直接跳到最近公共祖先上,会出现16和4都是u和v的公共祖先,这样16就满足了,但显然不是最近的。因此我们跳跃到最近公共祖先的子节点。最后答案就是这个点的父节点。

算法思路讲的差不多了,直接上代码吧,有些细节不清楚的结合代码理解就好。

AC代码:

/*
 * @Author: hesorchen
 * @Date: 2020-07-03 17:05:01
 * @LastEditTime: 2020-08-19 22:18:44
 * @Description: https://hesorchen.github.io/
 */
#include <map>
#include <set>
#include <list>
#include <queue>
#include <deque>
#include <cmath>
#include <stack>
#include <vector>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define endl '\n'
#define PI acos(-1)
#define PB push_back
#define ll int
#define INF 0x3f3f3f3f
#define mod 1000000007
#define pll pair<ll, ll>
#define lowbit(abcd) (abcd & (-abcd))
#define max(a, b) ((a > b) ? (a) : (b))
#define min(a, b) ((a < b) ? (a) : (b))

#define IOS                      \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0);
#define FRE                              \
    {                                    \
        freopen("in.txt", "r", stdin);   \
        freopen("out.txt", "w", stdout); \
    }

inline ll read()
{
    ll x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9')
    {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}
//head==============================================================================

ll depth[500010];
ll head[500010];
ll father[500010][35];
ll ct = 1;

struct node
{
    ll v, next;
} e[1000010]; //双向边开两倍!双向边开两倍!双向边开两倍!

void add(ll u, ll v) //链式前向星加边
{
    e[ct].v = v;
    e[ct].next = head[u];
    head[u] = ct++;
}

void dfs(ll now, ll fa, ll dep) //处理每个vertice的深度和第一个祖先
{
    depth[now] = dep;
    father[now][0] = fa;
    for (int i = 1; (1 << i) <= depth[now]; ++i) //dfs到点now时顺便处理father数组
        father[now][i] = father[father[now][i - 1]][i - 1];
    for (int i = head[now]; i; i = e[i].next)
        if (e[i].v != fa)
            dfs(e[i].v, now, dep + 1);
}
ll n, m, s;

ll lca(ll u, ll v) //计算lca
{

    if (depth[u] < depth[v])
        swap(u, v);
    ll cha = depth[u] - depth[v];
    for (int i = 0; i <= 30; i++) //和暴力解法一样,先把两个点的深度处理到一致,至于为什么循环到30看下一条注释
        if ((1 << i) & cha)
            u = father[u][i];

    if (v == u) //这种情况就是uv有一个是对方的祖先
        return u;

    for (int i = 30; i >= 0; i--) //这里我看别人代码用了log优化,其实数据在int范围内,不卡常的话从30开始足够了
        if (father[u][i] != father[v][i])//注意。不相等才跳跃,因为我们的目标不是直接跳到最近公共祖先上。看上面的红字。
        {
            u = father[u][i];
            v = father[v][i];
        }
    return father[u][0];
}
int main()
{

    IOS;
    cin >> n >> m >> s;
    for (int i = 1; i < n; i++) //建图
    {
        ll u, v;
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }

    dfs(s, 0, 1); //处理每个vertice的深度和祖先

    for (int i = 1; i <= m; i++)
    {
        ll u, v;
        cin >> u >> v;
        cout << lca(u, v) << endl;
    }
    return 0;
}
参考资料
  1. LCA及例题
  2. LCA详解
  3. LCA的倍增算法
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hesorchen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值