树上最近公共祖先(树上倍增)

本文介绍倍增算法在树形结构上的应用,详细解释如何通过预处理每个节点的2^j个祖先,实现LCA(最近公共祖先)问题的高效求解。文章包含完整的算法描述、状态转移方程及代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

算法描述

倍增算法在ST表中的应用是直接在第二维的dp中直接以二进制的形式展现;而对于树上的问题则可以这样设计:设fa[i][j]表示i结点的第2^j个祖先,显然的是fa[i][0]等于i的父节点,假设父节点的祖先结点都已经更新完毕,我们就可以根据父亲的祖先来更新本结点的祖先,于是状态转移方程就成了fa[i][j]=fa[fa[i][j-1]][j-1],这是根据二进制中可以拆分成两个相同小一次的二进制和的特性来构造的。于是,用 d f s dfs dfs把每个结点的fa处理好后求LCA就可以在 l o g n logn logn的复杂度求得。逻辑如下:

  • 将两个点提到一个深度(所以在dfs中还要维护每个结点的深度)
  • 两个结点一起向上跳,要从最多的步数然后越来越少地跳,如果两个祖先相等了可能是跳过头了,考虑不跳,然后不相等肯定没到达就选择往上跳,最后一定能到达LCA的儿子结点中,返回fa[u][0]即可。这里是根据二进制数-1可以由多个小于它的数组合而成的原理来构造的

例子

以洛谷P3379板子题来检验代码正确性

#include <bits/stdc++.h>
using namespace std;

struct Edge
{
    int to;
    int next;
};

Edge E[1000005];
int head[500005], cnt;

int n, m, rt;
int fa[500005][19]; //i结点的第2^j个祖先
int deep[500005];   //结点深度
int Log[500005];
int Mi[20];

void edge_add(int u, int v)
{
    E[cnt].to = v;
    E[cnt].next = head[u];
    head[u] = cnt++;
}

void init()
{
    memset(head, -1, sizeof(head));
    memset(fa, 0, sizeof(fa));
    cnt = 0;
    int u, v;
    for (int i = 1; i < n; i++)
    {
        scanf("%d%d", &u, &v);
        edge_add(u, v);
        edge_add(v, u);
    }
}

void dfs(int u, int parent) {
    fa[u][0] = parent; //直接双亲更新
    deep[rt]=deep[parent]+1;
    for (int cur = 1; Mi[cur] <= deep[u]; cur++)
        fa[u][cur]=fa[fa[u][cur-1]][cur-1]; //走到这个点说明前面的都已经更新完毕  利用前面的更新当前的
    for (int cur = head[u]; ~cur; cur=E[cur].next)
        if (E[cur].to^parent) //非父即子
            dfs(E[cur].to, u);
}

int LCA(int u, int v) {
    if (deep[u]<deep[v]) swap(u, v);
    while (deep[u]>deep[v]) //爬树
        u=fa[u][Log[deep[u]-deep[v]]]; //拆分深度差的二进制,尽量往上跳
    if (u == v) return v;
    for (int len=Log[deep[u]]; len>=0; len--)
        if (fa[u][len]^fa[v][len]) { //相等考虑不跳,可能跳过头了,显然假设这个步长是解的话剩余的二进制加起来等于这个长度的二进制-1,所以底下返回父亲结点成立
            u = fa[u][len];
            v=fa[v][len];
        }
    return fa[u][0];
}

int main()
{
    Log[0] = -1;
    for (int i = 1; i <= 500000; i++)
        Log[i] = Log[i >> 1] + 1;
    Mi[0] = 1;
    for (int i = 1; i < 20; i++)
        Mi[i] = Mi[i - 1] << 1;
    int v, u;
    scanf("%d%d%d", &n, &m, &rt);
    init();
    dfs(rt, 0); //预处理fa表和深度表方便之后计算
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d", &u, &v);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}

优化常数的写法

#include <bits/stdc++.h>
using namespace std;

/**
 * 常数最小
*/

int n, m, s;
int _to[1000005], _next[1000005], head[500005], cnt;
int deep[500005], fa[500005][19];
int Log[500005], Mi[20];

void edge_add(int u, int v) {
    _to[cnt] = v;
    _next[cnt] = head[u];
    head[u] = cnt++;
}

void init() {
    memset(head, -1, sizeof(head));
    cnt=0;
    int u, v;
    for (int i = 1; i < n; i++) {
        scanf("%d%d", &u, &v);
        edge_add(u, v);
        edge_add(v, u);
    }
    deep[0]=0; fa[0][0]=0;//根的虚拟祖先
}

void dfs(int rt, int parent) {
    fa[rt][0] = parent;
    deep[rt]=deep[parent]+1;
    for (int i = 1; Mi[i] <= deep[rt]; i++) //注意这里是Mi而不是i
        fa[rt][i]=fa[fa[rt][i-1]][i-1];
    for(int cur=head[rt]; ~cur; cur=_next[cur])
        if (_to[cur]^parent)
            dfs(_to[cur], rt);
}

int LCA(int u, int v) {
    if (deep[u] < deep[v]) swap(u, v);
    while (deep[u] > deep[v])
        u=fa[u][Log[deep[u]-deep[v]]];
    if (u == v) return v;
    for (int i=Log[deep[u]]; i >= 0; i--)
        if (fa[u][i]^fa[v][i]) {
            u=fa[u][i];
            v=fa[v][i];
        }
    return fa[u][0];
}

int main() {
    Log[0]=-1;
    for (int i = 1; i < 500005; i++)
        Log[i] = Log[i>>1]+1;
    Mi[0]=1;
    for (int i = 1; i < 20; i++)
        Mi[i] = Mi[i-1]<<1;
    scanf("%d%d%d", &n, &m, &s);
    init();
    dfs(s, 0);
    int u, v;
    for (int i = 0; i < m; i++) {
        scanf("%d%d", &u, &v);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小胡同的诗

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

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

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

打赏作者

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

抵扣说明:

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

余额充值