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

最近公共祖先(LCA)

时间:2023-07-19 23:01:30浏览次数:31  
标签:tarjan 祖先 LCA int depth 公共 include 节点 dis

向上标记法

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

倍增

时间复杂度 预处理:$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,为保证使用depth[f[a][k]]不出错,定义走不到的点的深度为0,便于区分
    depth[root] = 1; // 由于将走不到的点的深度定义为0,所以根节点的深度定义为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;
}

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;
}

标签:tarjan,祖先,LCA,int,depth,公共,include,节点,dis
From: https://blog.51cto.com/u_14882565/6781196

相关文章

  • 冷门科技 —— DFS 序求 LCA
    Updateon2023.7.17:该技巧目前已知的最早来源:skip2004。Updateon2023.7.17:比较时,取时间戳较小的结点也是正确的,不用记录深度。DFS序求LCA无论是从时间常数,空间常数还是好写程度方面均吊打欧拉序。定义DFS序表示对一棵树进行深度优先搜索得到的结点序列,而时间戳DFN......
  • LCA 离线tarjan算法
    tarjan算法是离线算法,它必须先将所有的要查询的点对存起来,然后在搜的时候输出结果。tarjan算法很经典,因为算法的思想很巧妙,利用了并查集思想,在dfs下,将查询一步一步的搜出来。伪代码如下:可以看到,对于我们已经保存好的查询,假设为(u,v),u为此时已经搜完的子树的根节点,v的位置就只有两种......
  • 公共Mono模块
    作用:让没有继承Mono的类可以开启协程,可以统一管理Update的更新。-------------------------------MonoManager------------------------------------usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.Events;publicclassMon......
  • 最近公共祖先(LCA)
    最近公共祖先(LCA)主要思路:一个节点的\(2^n\)的祖先是那个节点\(2^{n-1}\)的祖先的\(2^{n-1}\)的祖先也就是说\(f[x][n]=f[f[x][n-1]][n-1]\)还要计算\(log_2\)的值,记录深度#include<bits/stdc++.h>usingnamespacestd;constintMAXN=5e5+10;intfa[MAXN][30],lg[M......
  • 公共操作-推导式(集合、列表、字典)
    1'''2Python推导式(Comprehensions)是一种简洁而强大的语法,用于创建新的数据结构(如列表、集合和字典)或对现有数据结构进行转换。3它可以使用更简单的方式完成迭代、筛选、映射等操作。45'''6#1.循环生成列表7#1.1准备⼀个空列表8list1=[]9#1.2书......
  • 公共操作
    说明从字符串、列表、元组、字典等维度,找出其共同的操作,比如在运算符、公共方法、容器类型转换运算符 1'''2字符串,列表,字典,集合公共操作之运算符3主要用于字符串\列表\元组的拼接&&倍乘&成员判断4'''56#1.加号+7#1.1字符串拼接8str1=......
  • 公共-八股文
    跨域请求是什么,有什么问题,怎么解决客户端发起请求时,会检查请求的协议、域名、端口是否与当前一致,如果不一致就会出现跨域问题要处理该问题:1.请求:请求通过后台转发至真正的接口[夹一层转发层,利用后台转发,类似网关]2.响应:响应上面加上“access-control-allow-origin”......
  • 1644 题「二叉树的最近公共祖先 II
    对于这道题来说,p和q不一定存在于树中,所以你不能遇到一个目标值就直接返回,而应该对二叉树进行完全搜索(遍历每一个节点),如果发现p或q不存在于树中,那么是不存在LCA的。  ......
  • 题:二叉树中m是n的祖先,通过(后序遍历)可以找到m到n的路径
     选项:先序?中序?后序?层次? 题解:1.首先是对路径的解释:访问一个结点x时,栈中结点恰好是x结点的所有祖先,从栈底到栈顶所有结点加上x结点,就构成了从根结点到x结点的一条路径。2.分析:为什么不是先序遍历?(第一次做题时以为这个路径指的是遍历的结果,那先序自然就满足,但这个路径不是遍历......
  • springcloud - 工程相关步骤以及提取公共部分
    1.创建父工程 配置pom文件删除src文件2.创建子模块配置pom文件3.配置yml文件4.创建启动类5.业务实现当出现公共代码时可以进行提取 例如实体类或者通用工具类等,如下图,提取成一个单独的模块先点击clean  然后点击install,最后将包导入到需要的子模块中实现相互......