图论--最近公共祖先LCA

2023-11-19

最近公共祖先LCA

LCA(Least Common Ancestors),即最近公共祖先,是指这样一个问题:在有根树中,找出某两个结点u和v最近的公共祖先(另一种说法,离树根最远的公共祖先)
在这里插入图片描述

最近公共祖先是相对于两个节点来说的,一般来说,最近公共祖先为节点 u和节点 v的最近的公共祖先。若 u 为 v 的祖先或者 v 为 u 的祖先,则LCA(u,v) 就是作为祖先的那个节点。示例图中 86 和 67的 LCA 是 75 ,61和 85的 LCA也是 75 。

以下三种方法,后两种方法比较重要,需要熟记,再求后面次小生成树有很大帮助。

LCA转RMQ

RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j之间的最小/大值。这两个问题是在实际应用中经常遇到的问题,本文介绍了当前解决这两种问题的比较高效的算法。

RMQ算法

对于该问题,最容易想到的解决方案是遍历,复杂度是O(n)。但当数据量非常大且查询很频繁时,该算法也许会存在问题。

介绍了一种比较高效的在线算法(ST算法)解决这个问题。所谓在线算法,是指用户每输入一个查询便马上处理一个查询。该算法一般用较长的时间做预处理,待信息充足以后便可以用较少的时间回答每个查询。ST(Sparse Table)算法是一个非常有名的在线处理RMQ问题的算法,它可以在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。

首先是预处理,用动态规划(DP)解决。设A[i]是要求区间最值的数列,F[i, j]表示从第i个数起连续2^j个数中的最大值。例如数列3 2 4 5 6 8 1 2 9 7,F[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。 F[1,2]=5,F[1,3]=8,F[2,0]=2,F[2,1]=4……从这里可以看出F[i,0]其实就等于A[i]。这样,DP的状态、初值都已经有了,剩下的就是状态转移方程。我们把F[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从i到i+2(j-1)-1为一段,i+2(j-1)到i+2j-1为一段(长度都为2(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。F[i,j]就是这两段的最大值中的最大值。于是我们得到了动态规划方程F[i, j]=max(F[i,j-1], F[i + 2^(j-1),j-1])。

然后是查询。取k=[log2(j-i+1)],则有:RMQ(A, i, j)=min{F[i,k],F[j-2^k+1,k]}。 举例说明,要求区间[2,8]的最大值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最大值我们可以直接由f[2,2]和f[5,2]得到。

算法伪代码:
//初始化
INIT_RMQ
//max[i][j]中存的是重j开始的2^i个数据中的最大值,最小值类似,num中存有数组的值
for i : 1 to n
  max[0][i] = num[i]
for i : 1 to log(n)/log(2)
  for j : 1 to (n+1-2^i)
     max[i][j] = MAX(max[i-1][j], max[i-1][j+2^(i-1)]
//查询
RMQ(i, j)
k = log(j-i+1) / log(2)
return MAX(max[k][i], max[k][j-2^k+1])

LCA算法

对于该问题,最容易想到的算法是分别从节点u和v回溯到根节点,获取u和v到根节点的路径P1,P2,其中P1和P2可以看成两条单链表,这就转换成常见的一道面试题:【判断两个单链表是否相交,如果相交,给出相交的第一个点。该算法总的复杂度是O(n)(其中n是树节点个数)
在线算法DFS+ST描述(思想是:将树看成一个无向图,u和v的公共祖先一定在u与v之间的最短路径上):

(1)DFS:从树T的根开始,进行深度优先遍历(将树T看成一个无向图),并记录下每次到达的顶点。第一个的结点是root(T),每经过一条边都记录它的端点。由于每条边恰好经过2次,因此一共记录了2n-1个结点,用E[1, … , 2n-1]来表示。

(2)计算R:用R[i]表示E数组中第一个值为i的元素下标,即如果R[u] < R[v]时,DFS访问的顺序是E[R[u], R[u]+1, …, R[v]]。虽然其中包含u的后代,但深度最小的还是u与v的公共祖先。

(3)RMQ:当R[u] ≥ R[v]时,LCA[T, u, v] = RMQ(L, R[v], R[u]);否则LCA[T, u, v] = RMQ(L, R[u], R[v]),计算RMQ。

由于RMQ中使用的ST算法是在线算法,所以这个算法也是在线算法。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题:
7 6 7个数6条边
3 7
3 4
4 5
4 6
5 1
6 2
求2和7的lca.
假如我们要询问 2 和7 的 LCA, 我们找到2和7 分别第一次出现的位置, 然后在这一个区间内找到深度最小的那个节点, 也就是节点 3, 显然它就是2 和7的 LCA.
在这里插入图片描述

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,cnt,tot=0,x,y,head[505],vis[505],dfn[505],de[505],ofs[505],dp[505][9],ru[505],root;
struct Egde{
    int to,next;
}edge[505];//表示边 
void add_edge(int bg,int ed)
{
    cnt++;
    edge[cnt].to=ed;
    edge[cnt].next=head[bg];
    head[bg]=cnt;
}
void dfs(int u,int dep)
{
    tot++;
    if(!vis[u]){
        vis[u]=1;dfn[u]=tot;
    }
    de[tot]=dep;
    ofs[tot]=u;
    for(int e=head[u];e>0;e=edge[e].next)
    {
        int v=edge[e].to;
        dfs(v,dep+1);
        ofs[++tot]=u;//通过这句话tot加,使dfn[]增加
    }
}
void init()//dp[]表示哪个tot
{
    for(int j=0;(1<<j)<=tot;j++)
    {
        for(int i=1;i+(1<<j)<=tot;i++)
        {
            if(j==0) dp[i][j]=i;
            else //dp用来存从i开始的2的j次方个区间内的最小值 
            {
                if(de[dp[i][j-1]]<de[dp[i+(1<<(j-1))][j-1]])
                    dp[i][j]=dp[i][j-1];
                else dp[i][j]=dp[i+(1<<(j-1))][j-1];//注意这里的左移外括号 
            }
        }
    }
}
int RMQ(int p1,int p2)//p1,p2是位置 
{
    int k=0;
    k=log2(p2-p1+1);
    if(de[dp[p1][k]]<de[dp[p2-(1<<k)+1][k]])
        return ofs[dp[p1][k]];
    else return ofs[dp[p2-(1<<k)+1][k]];
}
int lca(int v1,int v2)
{
    if(dfn[v1]<dfn[v2]) return RMQ(dfn[v1],dfn[v2]);//4,12 
    else RMQ(dfn[v2],dfn[v1]);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        add_edge(x,y);
        ru[y]++;
    }
    for(int i=1;i<=n;i++)if(ru[i]==0) root=i;
    dfs(root,1);
    init();
//  for(int i=1;i<=n;i++) 
//      for(int j=head[i];j>0;j=edge[j].next)
//          cout<<edge[j].to<<":"<<dfn[edge[j].to]<<" ";
//  cout<<endl;
    cout<<lca(2,7)<<endl;
}

裸求LCA

假设目前求点u和v的最近公共祖先。
1、首先找到两点中深度较深的点(在树上的深度越深代表其越往下),不妨设深度较深的结点为u,不停的将u往上提,直到u的深度和v一样。
2、同时将u和v向上提,直到u和v变成了同一个点。这个点就是要求的醉经公共祖先。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int fa[N],d[N],n,m,rt;
vector<int>G[N];
void add(int u,int v)
{
	G[u].push_back(v);
	G[v].push_back(u);
 } 
void dfs(int u)//先计算每个点的深度
{
	for(int i = 0;i<G[u].size();i++)
	{
		int v = G[u][i];
		if(v==fa[u]) continue;
		fa[v]=u;//保存每个点的父节点 
		d[v]=d[u]+1;//子节点的深度比父节点多1
		dfs(v);
	}
 } 
 int LCA(int u,int v)
 {
 	if(d[u]<d[v]) swap(u,v);//找出深度最深的点,保存在u中
 	while(d[u]!=d[v]) u= fa[u];//先不停的让u变成它的父节点,往上提直到同一层
 	while(u!=v)
 	{
 		u=fa[u];
 		v=fa[v];
 		//之后再不断地同时往上提u,v,直到它们相同之后,这个点就是答案
	 }
	 return u;
 }
 int main()
 {
 	cin>>n>>m>>rt;
 	for(int i = 1;i<n;i++)
 	{
 		int u,v;
 		cin>>u>>v;
 		add(u,v);
	 }
	 dfs(rt);
	 for(int i = 1;i<=m;i++)
	 {
	 	int u,v;
	 	cin>>u>>v;
	 	cout<<LCA(u,v)<<endl;
	 }
 }

这个程序虽然正确计算了样例,但会超时。分析程序会发现,每次询问最近公共祖先的时候最坏的情况是爬完整棵树(想象一条链的情况),所以每次询问的时间复杂度是O(n)的。

倍增求LCA

在这里插入图片描述

以 17 和 18 为例,既然要求LCA,那么我们就让他们一个一个向上爬(我要一步一步往上爬 —— 《蜗牛》),直到相遇为止。第一次相遇即是他们的LCA。 模拟一下就是:
17->14->10->7->3
18->16->12->8->5->3
最终结果就是 3。

倍增算法

所谓倍增,就是按2的倍数来增大,也就是跳1,2,4,8,16,32 …… 不过在这我们不是按从小到大跳,而是从大向小跳,即按……32,16,8,4,2,1来跳,如果大的跳不过去,再把它调小。这是因为从小开始跳,可能会出现“悔棋”的现象。拿 5 为例,从小向大跳,5≠1+2+4,所以我们还要回溯一步,然后才能得出5=1+4;而从大向小跳,直接可以得出5=4+5=4+1。这也可以拿二进制为例,5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那我就不填,这个过程是很好操作的。

还是以 17 和 18 为例(此例只演示倍增,并不是倍增LCA算法的真正路径):
17->3
18->5->3
可以看出向上跳的次数大大减小。这个算法的时间复杂度为O(nlogn),已经可以满足大部分的需求。

想要实现这个算法,首先我们要记录各个点的深度和他们2^i
级的的祖先,用数组depth[]表示每个节点的深度,fa[i][j]表示节点i的2^j级祖先。 代码如下:

void dfs(int f,int fath) //f表示当前节点,fath表示它的父亲节点
{
depth[f]=depth[fath]+1;
fa[f][0]=fath;
for(int i=1;(1<<i)<=depth[f];i++)
  fa[f][i]=fa[fa[f][i-1]][i-1]; //这个转移可以说是算法的核心之一
                                //意思是f的2^i祖先等于f的2^(i-1)祖先的2^(i-1)祖先
                                //2^i=2^(i-1)+2^(i-1)
for(int i=head[f];i;i=e[i].nex)
  if(e[i].t!=fath)
    dfs(e[i].t,f);
}

预处理完毕后,我们就可以去找它的LCA了,为了让它跑得快一些,我们可以加一个常数优化。

for(int i=1;i<=n;i++) //预先算出log_2(i)+1的值,用的时候直接调用就可以了
  lg[i]=lg[i-1]+(1<<lg[i-1]==i);  //看不懂的可以手推一下

接下来就是倍增LCA了,我们先把两个点提到同一高度,再统一开始跳。

但我们在跳的时候不能直接跳到它们的LCA,因为这可能会误判,比如4和8,在跳的时候,我们可能会认为1是它们的LCA,但1只是它们的祖先,它们的LCA其实是3。所以我们要跳到它们LCA的下面一层,比如4和8,我们就跳到4和5,然后输出它们的父节点,这样就不会误判了。

int lca(int x,int y)
{
if(depth[x]<depth[y]) //用数学语言来说就是:不妨设x的深度 >= y的深度
  swap(x,y);
while(depth[x]>depth[y])
  x=fa[x][lg[depth[x]-depth[y]]-1]; //先跳到同一深度
if(x==y)  //如果x是y的祖先,那他们的LCA肯定就是x了
  return x;
for(int k=lg[depth[x]]-1;k>=0;k--) //不断向上跳(lg就是之前说的常数优化)
  if(fa[x][k]!=fa[y][k])  //因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。
    x=fa[x][k], y=fa[y][k];
return fa[x][0];  //返回父节点
}

完整的求17和18的LCA的路径:
17->10->7->3
18->16->8->5->3
解释:首先,18要跳到和17深度相同,然后18和17一起向上跳,一直跳到LCA的下一层(17是7,18是5),此时LCA就是它们的父亲。

Tarjan求LCA

Tarjan

int f[maxn],fs[maxn];//并查集父节点 父节点个数
bool vit[maxn];
int anc[maxn];//祖先
vector<int> son[maxn];//保存树
vector<int> qes[maxn];//保存查询
typedef vector<int>::iterator IT;
 
int Find(int x)
{
    if(f[x]==x) return x;
    else return f[x]=Find(f[x]);
}
void Union(int x,int y)
{
    x=Find(x);y=Find(y);
    if(x==y) return;
    if(fs[x]<=fs[y]) f[x]=y,fs[y]+=fs[x];
    else f[y]=x,fs[x]+=fs[y];
}
 
void lca(int u)
{
    anc[u]=u;
    for(IT v=son[u].begin();v!=son[u].end();++v)
    {
        lca(*v);
        Union(u,*v);
        anc[Find(u)]=u;
    }
    vit[u]=true;
    for(IT v=qes[u].begin();v!=qes[u].end();++v)
    {
        if(vit[*v])
            printf("LCA(%d,%d):%d\n",u,*v,anc[Find(*v)]);
    }
}

练习

1、求最近公共祖先
建议使用倍增和Tarjan两种方法写
2、最近公共祖先
3、仓鼠找Sugar
4、天天爱跑步

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

图论--最近公共祖先LCA 的相关文章

随机推荐

  • 使用STM32高级定时器(TIM8)PWM互补通道输出PWM

    一 为何使用 最近做项目 因为定时器不够用需要用高级定时器 TIM8 来输出PWM来控制电机 刚好硬件工程师把引脚分配到了TIM8定时器CH3的互补通道CH3 ON上 所以需要将CH3 ON当普通的PWM模式输出PWM 特意记录一下 二 下
  • 阿里云Linux热扩容云盘(growpart和resize2fs工具)

    阿里云linux机器系统盘空间不够进行扩容 一 扩容物理盘 阿里云控制台在线扩容完成 二 安装growpart工具和resize2fs工具 root A yum install cloud utils growpart root A yum
  • token保活设计.md

    如果我们要使用token机制用以标识用户登录状态 以获得请求相关资源接口的权限 让你来设计一套方案 以为怎么设计呢 通常有两种思路 1 使用refreshtoken获取新的accesstoken 登录成功之后 返回一个返回refreshto
  • jQuery动态控制单选框选中,实现radio单选框选中后触发事件。prop()选中,取消事件判断。

    input name IS BREAK value 0 prop checked true div class form group div
  • 重读百度移动生态:“第一曲线”的创新“延长线”

    刚刚结束的 WISE2022新经济之王 大会上 百度集团资深副总裁 百度移动生态事业群组总经理何俊杰在主旨演讲中断言 百度搜索 百度APP是AI规模最大的应用场景 随着AI预训练大模型 AIGC 数字人等新技术的规模化落地 其AI带来的创新
  • 【STM32】IIC使用中DMA传输时 发送数据总少一个的问题

    问题描述 在使用STM32 I2C数据发送过程中 发现每轮实际发送出去的数据总比在DMA配置中设定的传输数据个数要少一个 比方说 DMA配置里设定的传输数据个数是10个 结果发现在总线上只能发出9个 经过进一步发现是少了最后一个数据 当对I
  • 简单认识KLT(Kanade-Lucas-Tomasi )跟踪算法

    KLT Kanade Lucas Tomasi 跟踪算法 前言 研究目标跟踪的算法种类颇多 主要可分为两大类 一类是传统的目标跟踪算法 包括粒子滤波 pf Mean Shift及KLT算法 或称Lucas光流法 另一大类是基于深度学习的跟踪
  • 字节跳动笔试---字母交换,最多m次

    参考 https blog csdn net cxzzxc123456 article details 79058419 编码题 字符串S由小写字母构成 长度为n 定义一种操作 每次都可以挑选字符串中任意的两个相邻字母进行交换 询问在至多交
  • [ C语言练习题 5 ] 矩阵转置(将矩阵的行列互换得到的新矩阵)

    题目来源 矩阵转置 牛客题霸 牛客网 描述 KiKi有一个矩阵 他想知道转置后的矩阵 将矩阵的行列互换得到的新矩阵称为转置矩阵 请编程帮他解答 输入描述 第一行包含两个整数n和m 表示一个矩阵包含n行m列 用空格分隔 1 n 10 1 m
  • 使用库函数sqrt求平方根 C语言入门

    欢迎关注笔者 你的支持是持续更博的最大动力 目录 问题描述 库函数 头文件 代码 其他 问题描述 使用库函数sqrt求平方根 库函数 库文件 Library function C C 标准规定的 编译器自带的函数 库函数顾名思义就是库里已存
  • 【QML】如何在QML中添加自定义模块并使用

    一 引言 在 导入本地QML文档目录 一文中 记录了如何导入本地QML文档 本文将记录 如何在QML中使用自定义模块 二 过程记录 本文以一个backend目录来存放自定义的模块 在该目录放置一个Data目录作为模块 其中用于描述模块的就有
  • C语言递归类练习题目

    题目 1 递归和非递归分别实现求第n个斐波那契数 2 编写一个函数实现n k 使用递归实现 3 写一个递归函数DigitSum n 输入一个非负整数 返回组成它的数字之和 例如 调用DigitSum 1729 则应该返回1 7 2 9 它的
  • 如何在Ubuntu上面修改为清华源

    如何在 U b u n t u 上面修改为清华源
  • iOS UITabBartroller作为根视图

    RootViewController m UITabBarCOntrollerDemo Created by Dubai on 14 10 4 Copyright c 2015年 DUbai All rights reserved impo
  • CSS 浏览器缩小之后页面错乱 块不见问题

    问题1 浏览器正常100 显示的时候 今日推荐是看得见的 浏览器缩小 小于100 之后 今日推荐被挤不见了 今日推荐块的DIV的CSS原配置是 today recommend py container width 1200px margin
  • linux modules相关工具和命令

    L 一 管理内核模块的相关命令 1 lsmod 列加以挂载的内核模块 lsmod 是列出目前系统中已加载的模块的名称及大小等 另外我们还可以查看 proc modules 我们一样可以知道系统已经加载的模块 代码 root localhos
  • 这几个免费资源网站太强了!老司机们都收藏了!

    简介 这几个资源网站是我见过资源最牛 最全 最丰富的网站 1000000 00T都装不下 老司机们都震惊了 强烈建议老司机们收藏 关键是都是免费的 本篇文章可以用来免费看片 认真学习 安全开车 1 小纸条 开放存粹的资源网站 如图所示 资源
  • 电脑安装pandas报错_python3.8下如何解决pandas报错No module named '_bz2'问题

    1 说明 1 1 不知道哪里出问题了 在使用pandas时报错 不能使用 Python 3 8 0 default Mar 18 2020 21 36 59 GCC 6 3 0 20170516 on linuxType help copy
  • MySQL事务提交过程(二)

    异步2周年 技术图书免费选 程序员8月书讯 项目管理 代码托管 文档协作 开发更流畅 MySQL事务提交过程 二 2017 01 01 21 18 389人阅读 评论 0 收藏 举报 分类 MySql 43 上一篇文章我们介绍了在关闭bin
  • 图论--最近公共祖先LCA

    最近公共祖先LCA LCA Least Common Ancestors 即最近公共祖先 是指这样一个问题 在有根树中 找出某两个结点u和v最近的公共祖先 另一种说法 离树根最远的公共祖先 最近公共祖先是相对于两个节点来说的 一般来说 最近