首页 > 其他分享 >最近公共祖先

最近公共祖先

时间:2024-06-04 18:36:11浏览次数:12  
标签:fa 祖先 公共 int depth 最近 LCA 节点

公共祖先: 在一棵有根树上,若节点 \(F\) 是节点 \(x\) 的祖先,也是节点 \(y\) 的祖先,那么称 \(F\) 是 \(x\) 和 \(y\) 的公共祖先。

最近公共祖先(LCA): 在 \(x\) 和 \(y\) 的所有公共祖先中,深度最大的称为最近公共祖先,记为 \(LCA(x,y)\)。

image

LCA 显然有以下性质。

  1. 在所有公共祖先中,\(LCA(x,y)\) 到 \(x\) 和 \(y\) 的距离都是最短的。例如,在 \(e\) 和 \(g\) 的所有祖先中,\(c\) 距离更短。
  2. \(x\) 与 \(y\) 之间最短的路径经过 \(LCA(x,y)\)。例如,从 \(e\) 到 \(g\) 的最短路径经过 \(c\)。
  3. \(x\) 和 \(y\) 本身也可以是它们自己的公共祖先。若 \(y\) 是 \(x\) 的祖先,则有 \(LCA(x,y)=y\),如图中 \(d=lca(d,h)\)。

如何求 LCA?根据 LCA 的定义,很容易想到一个简单直接的方法:分别从 \(x\) 和 \(y\) 出发,一直向根节点走,第一次相遇的节点就是 \(LCA(x,y)\)。具体实现时,可以用标记法:首先从 \(x\) 出发一直向根节点走,沿路标记所有经过的祖先节点;把 \(x\) 的祖先标记完之后,然后再从 \(y\) 出发向根节点走,走到第一个被 \(x\) 标记的节点,就是 \(LCA(x,y)\)。标记法的时间复杂度较高,在有 \(n\) 个节点的树上求一次 \(LCA(x,y)\) 的时间复杂度为 \(O(n)\)。若有 \(m\) 次查询,总的时间复杂度为 \(O(mn)\),效率太低。

倍增法求 LCA

可以把标记法换一种方式实现,分为以下两个步骤。

  1. 先把 \(x\) 和 \(y\) 提到相同的深度。例如,\(x\) 比 \(y\) 深,就把 \(x\) 提到 \(y\) 的高度(既让 \(x\) 走到 \(y\) 的同一高度),如果发现 \(x\) 直接就跳到 \(y\) 的位置上了,那么就停止查找,否则继续下一步。
  2. 让 \(x\) 和 \(y\) 继续同步向上走,每走一步就判断是否相遇,相遇点就是 \(LCA(x,y)\) 停止。

上面的两个步骤,如果 \(x\) 和 \(y\) 都一步一步向上走,时间复杂度为 \(O(n)\)。如何改进?如果不是一步步走,而是跳着走,就能加快速度。如何跳?可以按 \(2\) 的倍数向上跳,即跳 \(1,2,4,8,\cdots\) 步,这就是倍增法,倍增法用“跳”的方法加快了上述两个步骤。

步骤 1

把 \(x\) 和 \(y\) 提到相同的深度。具体任务是:给定两个节点 \(x\) 和 \(y\),设 \(x\) 比 \(y\) 深,让 \(x\) “跳”到与 \(y\) 相同的深度。

因为已知条件是只知道每个节点的父节点,所以如果没有其他辅助条件,\(x\) 只能一步步向上走,没法“跳”。要实现“跳”的动作,必须提前计算出一些 \(x\) 的祖先节点,作为 \(x\) 的“跳板”。然而,应该提前计算出哪些祖先节点呢?如何通过这些预计算出的节点准确且高效地跳到一个任意给定的 \(y\) 的深度?这就是倍增法的精妙之处:预计算出每个节点的第 \(1,2,4,8,16,\cdots\) 个祖先,即按 \(2\) 倍增的那些祖先。

有了预计算出的这些祖先做跳板,能从 \(x\) 快速跳到任何一个给定的目标深度。以从 \(x\) 跳到它的第 \(27\) 个祖先为例:

  1. 从 \(x\) 跳 \(16\) 步,到达 \(x\) 的第 \(16\) 个祖先 \(fa_1\);
  2. 从 \(fa_1\) 跳 \(8\) 步,到达 \(fa_1\) 的第 \(8\) 个祖先 \(fa_2\);
  3. 从 \(fa_2\) 跳 \(2\) 步到达祖先 \(fa_3\);
  4. 从 \(fa_3\) 跳 \(1\) 步到达祖先 \(fa_4\)。

共跳了 \(16+8+2+1=27\) 步,这个方法利用了二进制的特征:任何一个数都可以由 \(2\) 的倍数相加得到。\(27\) 的二进制是 \(11011\),其中的 \(4\) 个 \(1\) 的权值就是 \(16,8,2,1\)。

显然,用倍增法从 \(x\) 跳到某个 \(y\) 的时间复杂度为 \(O(\log n)\)。

剩下的问题是如何快速预计算每个节点的“倍增”的祖先。定义 \(fa_{x,i}\) 为 \(x\) 的第 \(2^i\) 个祖先,有非常巧妙的递推关系:\(fa_{x,i}=fa_{fa_{x,i-1},i-1}\)。分两步理解:\(fa_{x,i-1}\) 从 \(x\) 起跳,先跳 \(2^{i-1}\) 步,记这个点为 \(z\);再从 \(z\) 跳 \(2^{i-1}\) 步,一共跳了 \(2^{i-1}+2^{i-1}=2^i\) 步。

特别地,\(fa_{x,0}\) 是 \(x\) 的第 \(2^0=1\) 个祖先,就是 \(x\) 的父节点。\(fa_{x,0}\) 是递推式的初始条件,从它开始递推出了所有的 \(fa_{x,i}\)。递推的计算量有多大?从任意节点 \(x\) 到根节点,最多只有 \(\log n\) 个祖先,所以只需要递推 \(O(\log n)\) 次。所以整个 \(fa\) 的计算时间复杂度为 \(O(n \log n)\)。

步骤 2

经过上一个步骤,\(x\) 和 \(y\) 现在位于同一深度,让它们同步向上跳,就能找到它们的公共祖先。\(x\) 和 \(y\) 的公共祖先有很多,LCA(x, y) 是距离 \(x\) 和 \(y\) 最近的那个,其他祖先都更远。

从一个节点跳到根节点,最多跳 \(\log n\) 次。现在从 \(x,y\) 出发,从最大的 \(i \approx \log n\) 开始,跳 \(2^i\) 步,分别跳到 \(fa_{x,i},fa_{y,i}\),它们位于非常靠近根节点的位置(\(2^i \approx n\)),有以下两种情况:

  1. \(fa_{x,i}=fa_{y,i}\),这是一个公共祖先,它的深度小于或等于 LCA(x, y),这说明跳过头了,退回去换一个小的 \(i-1\) 重新跳一次。
  2. \(fa_{x,i} \ne fa_{y,i}\),说明还没跳到公共祖先,那么更新 \(x \leftarrow fa_{x,i}, y \leftarrow fa_{y,i}\),从新的起点 \(x,y\) 继续开始跳。由于新的 \(x,y\) 的深度比原来位置的深度减少超过一半,再跳时就不用跳 \(2^i\) 步,跳 \(2^{i-1}\) 步就够了。

以上两种情况,分别是比 LCA(x, y) 浅和深的两种位置。用 \(i\) 循环判断以上两种情况,就是从深浅两侧逐渐逼近 LCA(x, y)。每循环一次,\(i\) 减一,当 \(i\) 减为 \(0\) 时,\(x\) 和 \(y\) 正好位于 LCA(x,y) 的下一层,则 \(fa_{x,0}\) 就是 LCA(x,y)。

查询一次 LCA 的时间复杂度是多少?这里的 \(i\) 会从 \(\log n\) 递减到 \(0\),循环 \(O(\log n)\) 次。

倍增法的总计算量包括预计算 \(fa\) 和查询 \(m\) 次 LCA,总时间复杂度为 \(O(n \log n + m \log n)\)。

例题:P3379 【模板】最近公共祖先(LCA)

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 500005;
const int LOG = 19;
vector<int> tree[N];
int depth[N], fa[N][LOG];
void dfs(int cur, int pre) { 
    depth[cur] = depth[pre] + 1; // 深度比父节点深度多1
    for (int nxt : tree[cur]) { // 遍历所有邻居节点
        if (nxt != pre) { // 除了父节点以外都是子节点
            fa[nxt][0] = cur; // 记录父节点fa[][0]
            dfs(nxt, cur);
        }
    }
}
int lca(int x, int y) {
    if (depth[x] < depth[y]) swap(x, y); // 保证x深度更大
    // 将x和y提到相同高度
    int delta = depth[x] - depth[y];
    for (int i = LOG - 1; i >= 0; i--) 
        if (delta & (1 << i)) x = fa[x][i];
    if (x == y) return x; // 如果提到相同深度后已经重合,则直接返回
    // x和y同步往上跳
    for (int i = LOG - 1; i >= 0; i--) 
        if (fa[x][i] != fa[y][i]) { // 如果祖先相等,说明跳过头了,换一个小的i继续尝试
            x = fa[x][i]; y = fa[y][i]; // 如果祖先不相等,就更新x和y继续跳
        }
    // 最终x和y位于LCA的下一层,此时x或y的父节点就是LCA
    return fa[x][0]; 
}
int main()
{
    int n, m, s; scanf("%d%d%d", &n, &m, &s);
    for (int i = 1; i < n; i++) {
        int x, y; scanf("%d%d", &x, &y);
        // 建树
        tree[x].push_back(y); tree[y].push_back(x);
    }
    dfs(s, 0); // 预处理深度等信息
    for (int i = 1; i < LOG; i++)
        for (int j = 1; j <= n; j++)
            fa[j][i] = fa[fa[j][i - 1]][i - 1]; // 从fa[][0]开始递推
    while (m--) {
        int a, b; scanf("%d%d", &a, &b); printf("%d\n", lca(a, b));
    }
    return 0;
}

例题:P4180 [BJWC2010] 严格次小生成树

#include <cstdio>
#include <algorithm>
#include <vector>
using std::sort;
using std::swap;
using std::min;
using std::max;
using std::vector;
typedef long long LL;
const int N = 1e5 + 5;
const int M = 3e5 + 5;
const int LOG = 17;
const LL INF = 1e15;
struct Edge {
    int x, y, z;
};
Edge edges[M];
bool mst[M];
vector<Edge> tree[N];
int root[N], depth[N], fa[N][LOG], w1[N][LOG], w2[N][LOG];
int query(int x) {
    return root[x] == x ? x : root[x] = query(root[x]);
}
void update(int& mx1, int& mx2, int a1, int a2, int b1, int b2) {
    mx1 = max(a1, b1);
    mx2 = a1 == b1 ? max(a2, b2) : max(min(a1, b1), max(a2, b2));
}
void dfs(int u, int pre) {
    depth[u] = depth[pre] + 1;
    fa[u][0] = pre;
    for (Edge e : tree[u]) {
        int v = e.y, w = e.z;
        if (v == pre) continue;
        w1[v][0] = w;
        dfs(v, u);
    }
}
LL lca(int x, int y, int w, LL sum) {
    if (depth[x] < depth[y]) swap(x, y);
    int delta = depth[x] - depth[y];
    int res1 = 0, res2 = 0;
    for (int i = LOG - 1; i >= 0; i--) 
        if (delta & (1 << i)) {
            update(res1, res2, res1, res2, w1[x][i], w2[x][i]);
            x = fa[x][i];
        }
    if (x == y) {
        return w == res1 ? (res2 == 0 ? INF : sum + w - res2) : (res1 == 0 ? INF : sum + w - res1);
    }
    int tmp1 = 0, tmp2 = 0;
    for (int i = LOG - 1; i >= 0; i--) 
        if (fa[x][i] != fa[y][i]) {
            update(tmp1, tmp2, w1[x][i], w2[x][i], w1[y][i], w2[y][i]);
            update(res1, res2, res1, res2, tmp1, tmp2);
            x = fa[x][i]; y = fa[y][i];
        }
    update(tmp1, tmp2, w1[x][0], w2[x][0], w1[y][0], w2[y][0]);
    update(res1, res2, res1, res2, tmp1, tmp2);
    return w == res1 ? (res2 == 0 ? INF : sum + w - res2) : (res1 == 0 ? INF : sum + w - res1);
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) root[i] = i;
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%d", &edges[i].x, &edges[i].y, &edges[i].z);
    }
    sort(edges + 1, edges + m + 1, [](Edge& e1, Edge& e2) { 
        return e1.z < e2.z;
    });
    LL sum = 0;
    for (int i = 1; i <= m; i++) {
        int x = edges[i].x, y = edges[i].y, z = edges[i].z;
        int qx = query(x), qy = query(y);
        if (qx != qy) {
            root[qx] = qy; sum += z; mst[i] = true;
            tree[x].push_back({x, y, z});
            tree[y].push_back({y, x, z});
        }
    }
    dfs(1, 0);
    for (int i = 1; i < LOG; i++) 
        for (int j = 1; j <= n; j++) {
            fa[j][i] = fa[fa[j][i - 1]][i - 1];
            int a1 = w1[j][i - 1], a2 = w2[j][i - 1];
            int b1 = w1[fa[j][i - 1]][i - 1], b2 = w2[fa[j][i - 1]][i - 1];
            update(w1[j][i], w2[j][i], a1, a2, b1, b2);
        }
    LL ans = INF;
    for (int i = 1; i <= m; i++) {
        int x = edges[i].x, y = edges[i].y, z = edges[i].z;
        if (x == y) continue;
        if (!mst[i]) ans = min(ans, lca(x, y, z, sum));
    }
    printf("%lld\n", ans);
    return 0;
}

标签:fa,祖先,公共,int,depth,最近,LCA,节点
From: https://www.cnblogs.com/ronchen/p/18061609

相关文章

  • 2024年公共事务管理与社会服务国际会议(ICPAMSS2024)
    2024年公共事务管理与社会服务国际会议(ICPAMSS2024)会议简介2024国际公共事务管理与社会服务会议(ICPAMSS2024)将在广州隆重举行。本次盛会诚挚邀请来自世界各地的公共事务管理和社会服务领域的专家、学者和从业者齐聚一堂,探索行业发展前沿,分享实践经验,推动理论创新。会议将......
  • 分享下最近基于Avalonia UI和MAUI写跨平台时间管理工具的体验
    起因几个月前,我在寻找一款时间管理软件,类似番茄时钟的工具,但是希望可以自定义时间。需要自定义的场景做雅思阅读,3篇文件需要严格控制时间分配,需要一个灵活的计时器定期提醒,每30分钟需要喝水或者上个厕所或者摸一下鱼...总结起来就是:专注一段时间,比如30分钟,然后休息10分钟,......
  • AOP简化公共属性create_time,create_user,update_time,update_user记录的重复代码
    处理这些公共字段,需要在每一个业务方法中进行操作,编码相对冗余、繁琐使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。1.2实现思路在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重......
  • 代码随想录算法训练营第二十二天 | 235.二叉搜索树的最近公共祖先 701.二叉搜索树中的
    235.二叉搜索树的最近公共祖先题目链接文章讲解视频讲解思路:递归遍历二叉搜索树   如果当前值大于p和q的值,向左遍历   如果当前值小于p和q的值,向右遍历   否则说明当前值介于p和q之间,直接返回当前节点classSolution{public:TreeNode*lowestCommonAnc......
  • 最近的一些WP/ DASCTF/ RCTF
    最近欠了不少的wp没发捏……ctf成分逐渐减少哈哈......
  • 苹果电脑如何清理最近打开的文稿记录 Mac如何移除浏览痕迹保护隐私
    日常使用苹果电脑的过程中,我们经常会打开各种文稿,浏览网页等操作。然而,这些操作可能会留下一些记录,涉及到个人隐私和数据安全问题。下面我们来看看苹果电脑如何清理最近打开的文稿记录,Mac如何移除浏览痕迹保护隐私的相关内容。一、苹果电脑如何清理最近打开的文稿记录苹果电......
  • LeetCode 第14题:最长公共前缀题目解析(进阶版)
    本文我们来探索LeetCode第14题——最长公共前缀题目解析(进阶版)。文章目录引言题目介绍解题思路思路1:水平扫描法思路2:垂直扫描法思路3:分治法思路4:二分查找法思路5:字典树(Trie)水平扫描法详细解析步骤1:初始化前缀步骤2:逐个比较示例讲解Java代码实现图......
  • 隐私计算在智能城市建设中的应用:平衡公共安全与个人隐私
    PrimiHub一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。随着智能城市建设的快速推进,各种数据采集技术和设备在城市管理中的应用越来越广泛。这些技术和设备在提升城市管理效率、优化资源分配和提高公......
  • 图论-最近公共祖先
    例题:祖孙询问 给定一棵包含 n......
  • 【python007】读取csv文件url多进程下载图片数据(最近更新中)
    1.熟悉、梳理、总结项目研发实战中的Python开发日常使用中的问题、知识点等2.欢迎点赞、关注、批评、指正,互三走起来,小手动起来!3.欢迎点赞、关注、批评、指正,互三走起来,小手动起来!4.欢迎点赞、关注、批评、指正,互三走起来,小手动起来!......