LCA 最近公共祖先(RMQ、树上倍增、Tarjan),树上两点距离,线段重合长度

本文介绍了LCA算法的几种实现方式,包括使用RMQ算法优化区间查询、树上倍增法寻找最近公共祖先以及Tarjan算法结合并查集和DFS解决LCA问题。同时,文章还讨论了如何利用LCA算法来求解树上两点的距离和线段重合长度等问题。

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

对于LCA的一些理解

RMQ
  • dfs处理树
    对于一个树形结构,可以用dfs将一颗树转化成数组,数组中记录每个点的标号,这样数组就按照dfs的顺序把树存了下来

  • 确定祖先的范围
    对于询问的节点X和Y, X、Y的祖先一定存在于数组中X、Y第一次出现的区间内,而且祖先就是区间内深度最小的节点

  • RMQ
    dfs的复杂度就是树边数的二倍,所以区间查询需要优化,这里可以用RMQ算法,预处理结果O(1)得到最值

void dfs(int pre, int x, int step) {
    depth[x] = step;
    first[x] = cnt;
    tree[cnt] = x;
    cnt++;
    int len = g[x].size();
    for (int i = 0; i < len; ++i) {
        if (g[x][i] == pre) continue;
        dfs(x, g[x][i], step + 1);
        tree[cnt++] = x;
    }
    return;
}
int Min(int a, int b) {
    if (depth[a] < depth[b])    return a;
    else    return b;
}
void RMQ() {
    for (int i = 1; i <= cnt; ++i) {
        dp[i][0] = tree[i];
    }
    for (int i = 1; (1 << i) <= cnt; ++i) {
        for (int j = 1; j + (1 << i) - 1 <= cnt; ++j) {
            dp[j][i] = Min(dp[j][i - 1], dp[j + (1 << (i - 1))][i - 1]);
        }
    }
}
int LCA(int a, int b) {
    int l = first[a];
    int r = first[b];
    if (l > r)  swap(l, r);
    int len = log2(r - l + 1);
    return Min(dp[l][len], dp[r - (1 << len) + 1][len]);
}
树上倍增

father[ i ] [ j ]表示 i 的第 2 ^ j 个父亲

  • dfs
    dfs在处理深度的同时更新每个人的father数组
void dfs(int pre, int x) {
    // 更新father
    for (int i = 1; i <= log2(n); ++i) {
        if (fa[x][i - 1] == 0)  break;
        fa[x][i] = fa[fa[x][i - 1]][i - 1];
    }
    // 记录深度
    depth[x] = depth[pre] + 1;
    int len = g[x].size();
    for (int i = 0; i < len; ++i) {
    	int son = g[x][i];
        if (son == pre) continue;
        fa[son][0] = x;
        dfs(x, son);
    }
}
  • LCA
    首先将两个节点的深度调成一样,如果深度相同而且是同一个点就找到最近的祖先了,如果深度相同但是节点不同,这时就要向上倍增,需要注意的是倍增的中止条件不是两个节点相同,而是节点的父亲相同。因为每次倍增的范围很大,很可能超过最近公共祖先,我们可以从最大的祖先开始,如果祖先相同缩小范围,如果祖先不同更新两个点的状态继续找,最后两个人的最近公共祖先一定是father[ i ][ 0 ]
int LCA(int a, int b) {
    if (depth[a] < depth[b])  swap(a, b);
    int depth_x = depth[a] - depth[b];
    // 先将两个点的深度调成一样
    for (int i = 0; i <= log2(depth_x); ++i) {
        if (depth_x & (1 << i))
            a = fa[a][i];
    }
    // 如果深度一样,而且相同直接返回
    if (a == b) return a;
    // 从最远的父亲开始
    for (int i = log2(depth[a]); i >= 0; --i) {
        // 如果父亲不同就向上更新
        if (fa[a][i] != fa[b][i]) {
            a = fa[a][i];
            b = fa[b][i];
        }
    }
    return fa[a][0];
}
Tarjan

完美结合并查集dfs 在**O(n + q)**解决LCA问题。
dfs的时候需要做以下处理:

  • 对于当前点x来说:如果存在询问(x,y),而且y已经dfs访问过,那么LCA就是 find(y)
  • 回溯的时候要将son的祖先设置为x(子树的祖先始终为根节点
int n;
LL dp[maxn], ans[maxn];
int pre[maxn], vis[maxn];
struct ac{
    int v, d;
};
vector<ac> G[maxn], Q[maxn];

int find(int x) {
    return x == pre[x] ? x : pre[x] = find(pre[x]);
}

void Tarjan(int fa, int x) {
    vis[x] = 1;

    // 更新询问
    for (auto it : Q[x]) {
        if (!vis[it.v]) continue;
        ans[it.d] = dp[x] + dp[it.v] - 2 * dp[find(it.v)];
    }

    for (auto it : G[x]) {
        if (it.v == fa) continue;
        dp[it.v] = dp[x] + it.d;
        Tarjan(x, it.v);
        pre[it.v] = x; // 更新祖先
    }

    // for (int i = 0; i < (int)Q[x].size(); ++i) {
    // 	if (!vis[ Q[x][i].v ]) continue;
    // 	ans[ Q[x][i].d ] = dp[x] + dp[ Q[x][i].v ] - 2 * dp[find( Q[x][i].v )];
    // }
    // for (int i = 0; i < (int)G[x].size(); ++i) {
    // 	int son = G[x][i].v;
    // 	if (son == fa) continue;
    // 	dp[ G[x][i].v ] = dp[x] + G[x][i].d;
    // 	Tarjan(x, son);
    // 	pre[ G[x][i].v ] = x; 
    // }
}
求树上两点的距离

树上两点的距离是唯一的,距离 = depth[X] + depth[Y] - 2 * depth[ancestor]

线段重合的长度

思想和求距离一样,例如A到C和B到C的重合长度 = (dis_ac + dis_bc - dis_ab)/ 2

int dis(int a, int b) {
    int ancestor = LCA(a, b);
    int depth_a = depth[a];
    int depth_b = depth[b];
    int depth_ancestor = depth[ancestor];
    return depth_a + depth_b - 2 * depth_ancestor;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值