zoukankan      html  css  js  c++  java
  • 最近公共祖先(LCA)

    方法1:向上标记法

    时间复杂度(O(n))
    待查询点为a和b,首先从a点向根节点进行搜索,将路径上的点进行标记,
    再从b点向根节点进行搜索,同时检测路径上的点是否被标记过,第一次检测到的点即为a,b两点的最近公共祖先

    方法2:倍增

    时间复杂度
    预处理:(O(nlog_n))
    查询: (O(log_n))

    相关概念及性质

    1. 节点祖先:从根到该节点所经分支上的所有节点,从根节点到此节点的路径中除了此节点本身(但是在最近公共祖先中的祖先可以是节点本身),其余节点均为此节点的祖先节点,(儿子->父亲->父亲的父亲->...->根
    2. 深度:对于任意节点n,n的深度为从根到n的唯一路径长度,根的深度定为0(为了算法实现的方便,根的深度一般定义为1)

    实现思路
    为了便于说明算法流程,以一道实际题目为例

    (depth[i]):节点(i)的深度为(depth[i])
    (f[i][k]):节点i走(2^k)步所能到的点为(f[i][k])

    预处理以上两个数组,之后对于查询(lca(a, b))

    1. 先将两个点跳到同一层((depth[a] == depth[b]))
    for (int k = 15; ~k; -- k) // 题中最多40000个点,0~15位二进制最大表示数据为 2^16-1=65535>40000
        if (depth[f[a][k]] >= depth[b]) // 判断要跳到的目标位置与b的位置关系
            a = f[a][k];
    
    1. 如果此时两个点不相同,让两个点同时向上跳,一直跳到它们最近公共祖先的下一层;反之,说明它们本身就为祖孙关系
      这里利用二进制拼凑的思想,此时两个点的层数是相同的,目标位置均为最近公共祖先的下一层,故两者要跳的距离也是相同的。设此值为dis

    计算一个数的二进制组成时,应当从大数向小数进行尝试.eg:对于11,应当从16开始判断,一直到1
    16 8 4 2 1
    0 1 0 1 1

    按照上述方式,两点初始步幅最大,之后步幅逐渐减小,最终一定可以拼凑出dis步,两点一定可以走到目标位置

    从大到小遍历所有步幅,如果两者能够走到同一个点((f[a][k] == f[b][k])),不走,不能走到同一个点((f[a][k] != f[b][k]))才走

    因为(f[a][k] == f[b][k])只能满足该位置是两者的公共祖先,而非最近公共祖先,所以把目标位置定为最近公共祖先的下一层,便于代码实现

    if (a == b) return a;
    for (int k = 15; ~k; -- k) // 15的理由同上
        if (f[a][k] != f[b][k])
        {
            a = f[a][k];
            b = f[b][k];
        }
    
    1. 如果此时两个点不相同,那么任意一个点再向上走1步即为两点的最近公共祖先
    return f[a][0];
    

    上述题目代码实现

    #include <iostream>
    #include <cstring>
    #include <cstdio>
    #include <algorithm>
    #include <queue>
    
    using namespace std;
    
    const int N = 4e4 + 10, M = N * 2;
    
    int n, m;
    int h[N], e[M], ne[M], idx;
    int depth[N], f[N][16];
    queue<int> q;
    
    void add(int a, int b)
    {
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx ++;
    }
    // 更新depth和f
    bool bfs(int root)
    {
        /** 这里depth起到2个作用:
         * 1. 保存某个点的深度
         * 2. 标记某个点是否已经被搜索过(同样可以通过引入st数组)
         * 将depth初始化为无穷大,通过判断是否为无穷大判断某个点是否被搜索过,depth的初始值只需要是一个非正常值,能够区分已搜索点和未搜索点即可
         */
    
        memset(depth, 0x3f, sizeof depth);
        depth[0] = 0; // f[a][k]当k较大时,从a点出发可能超出了根节点,即f[a][k]为0,定义走不到的点的深度为0,便于区分
        depth[root] = 1;
        q.push(root);
    
        while (q.size())
        {
            int t = q.front();
            q.pop();
            for (int i = h[t]; ~i; i = ne[i])
            {
                int p = e[i];
                if (depth[p] == 0x3f3f3f3f)
                {
                    q.push(p);
    
                    depth[p] = depth[t] + 1;
    
                    f[p][0] = t;
                    for (int k = 1; k <= 15; ++ k) // f[p][k] 需要用到f[p][k - 1],故需要从小到大
                        f[p][k] = f[f[p][k - 1]][k - 1];
                }
            }   
        }
    }
    int lca(int a, int b)
    {
        if (depth[a] < depth[b]) swap(a, b); // 保证a下b上
        for (int k = 15; ~k; -- k)
            if (depth[f[a][k]] >= depth[b]) // depth[0] = 0,决定了这里从a点出发不会跳转到b点之上
                a = f[a][k];
        if (a == b) return a;
        for (int k = 15; ~k; -- k)
            if (f[a][k] != f[b][k])
            {
                a = f[a][k];
                b = f[b][k];
            }
        return f[a][0];
    }
    int main()
    {
        int root;
        memset(h, -1, sizeof h);
        cin >> n;
        while (n --)
        {
            int a, b;
            cin >> a >> b;
            if (b == -1) root = a;
            else add(a, b), add(b, a);
        }
    
        bfs(root);
    
        cin >> m;
        while (m --)
        {
            int a, b;
            cin >> a >> b;
            int p = lca(a, b);
            if (p == a) cout << 1 << endl;
            else if (p == b) cout << 2 << endl;
            else cout << 0 << endl;
        }
        return 0;
    }
    

    方法3:Tarjan

    时间复杂度(O(n + m))

    实现思路
    深度优先遍历,将点分类
    [1] 正在搜索的点
    [2] 已经遍历且回溯过的点
    [3] 还未搜索的点

    算法有些抽象以一道题目为例进行解释

    //      *
    //      |1
    //      * 
    //    2/ 3
    //    *   *
    /**
     * 两个叶子节点距离 = (2 + 1) - (3 + 1) - 2 * 1
     * Tarjan离线
     */
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <cstdio>
    #include <queue>
    #include <vector>
    
    using namespace std;
    using PII = pair<int, int>;
    
    const int N = 1e4 + 10, M = N * 2;
    
    int n, m;
    int h[N], e[M], ne[M], w[M], idx;
    int p[N];
    queue<int> q;
    int res[M];
    int st[N];
    int dis[N];
    vector<PII> query[M];
    
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void add(int a, int b, int c)
    {
        e[idx] = b;
        ne[idx] = h[a];
        w[idx] = c;
        h[a] = idx ++;
    }
    // 初始化各点与根节点的距离
    void bfs(int root)
    {
        memset(dis, 0x3f, sizeof dis);
        dis[root] = 0;
        q.push(root);
    
        while (q.size())
        {
            int t = q.front();
            q.pop();
    
            for (int i = h[t]; ~i; i = ne[i])
            {
                int j = e[i];
                if (dis[j] == 0x3f3f3f3f)
                {
                    dis[j] = dis[t] + w[i];
                    q.push(j);
                }
            }
        }
    }
    /**
     * tarjan难在回溯
     * 很多操作之间都存在严格的先后关系,由于结合递归回溯,难理解很多
     */
    void tarjan(int x)
    {
        st[x] = 1; // 搜索开始
        // 遍历u节点下的所有子节点
        for (int i = h[x]; ~i; i = ne[i])
        {
            int j = e[i];
            if (!st[j]) // st = 0表示不是正在搜索且不是已经搜索完成,是还未搜索
            {
                tarjan(j);
                p[j] = x;
            }
        }
    
        for (auto question : query[x])
        {
            int y = question.first, id = question.second;
            if (st[y] == 2)
            {
                int ancestor = find(y);
                /**
                 * find(y)为什么是最近公共祖先?
                 * 这个和tarjan(j),p[j] = x的先后顺序有关,只有在把以x节点为根节点的子树全部搜索完成后,
                 * x节点的p[x]才会更新会上层节点,所以x节点以下的节点所找到的父节点都是x而非整棵树的根节点
                 * 比较适合理解这个问题的例子
                 *         a
                 *         |
                 *         b
                 *        / 
                 *       c   d
                 * 询问(c, d)距离
                 * tarjan(a) -> tarjan(b) -> tarjan(c), p[c] = b; tarjan(d)
                 * p[a] = a     p[b] = b      p[c] = b
                 * 因为在d那一层计算(c,d)距离时,p[b] = b并没有更新,但c点的tarjan已经结束
                 * 所以p[c]已经更新为b,所以find(c) = b,是c与d的最近公共祖先,而非根节点a
                 */
                res[id] = dis[x] + dis[y] - 2 * dis[ancestor];
            }
        }
    
        st[x] = 2; // 搜索完成
    }
    int main()
    {
        memset(h, -1, sizeof h);
        cin >> n >> m;
        for (int i = 0; i < n - 1; ++ i)
        {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, c), add(b, a, c);
        }
    
        for (int i = 0; i < m; ++ i)
        {
            int a, b;
            cin >> a >> b;
            /**
             * 存储了2次
             * 答案存储依据的是问题编号i,所以多存储并不会影响最终结果
             * 需要存储2次的原因是距离的计算只有在回溯的时候可以计算出来
             * 我们无法确定点与点之间的位置关系,所以需要存储2次数
             */
            query[a].push_back({b, i});
            query[b].push_back({a, i});
        }
    
        for (int i = 1; i <= n; ++ i) p[i] = i;
    
        bfs(1); // 初始化各节点距离根节点距离
        tarjan(1);
    
        for (int i = 0; i < m; ++i) cout << res[i] << endl;
    
        return 0;
    }
    
  • 相关阅读:
    小程序官方请求封装
    小程序天/小时/分秒倒计时封装
    小程序不定数量左右滑动中间放大轮播图效果
    小程序换行符检测换行
    小程序点击图片重新排序写法
    基于webuploader.js的单图片上传封装
    VMware Fusion 11 序列号
    Ionic 4 beta + Capacitor beta 尝鲜
    C语言学习笔记之动态分配数组空间
    C语言学习笔记之获取文件长度
  • 原文地址:https://www.cnblogs.com/G-H-Y/p/15226600.html
Copyright © 2011-2022 走看看