深度剖析倍增算法求解最近公共祖先(LCA)的细枝末节

news2025/1/16 15:54:38

1. LCA(最近公共祖先)

倍增算法的基本思想在前面的博文中有较详细的介绍,本文不再复述。此文仅讲解如何使用倍增算法求解多叉树中节点之间的最近公共祖先问题。

什么是最近公共祖先问题?

字面而言,指在树上查询两个(也可以是两个以上)节点的祖先,且是离两个节点最近的祖先。如下图所示:

  • 节点 12和节点11的公共祖先有节点4和节点1
  • 节点4是离12和11最近的祖先。即1211的最近公共祖先是4。也可描述为LCA(12,11)=3

Tips: LCA是(Lowest Common Ancestor 最近公共祖先)的简称。

1.png

LCA有如下几个特性:

  • LCA(u)=u。单个节点的的最近公共祖先为自己。如上图节点 12的最近公共祖先为 12,即LCA(12)=12

  • 如果 uv的祖先,当且仅当LCA(u,v)=u。如上图,LCA(1,2)=1

  • 如果 u不是v 的祖先,并且 v 不是 u 的祖先,那么 u,v 分别处于 LCA(u,v) 的两棵不同子树中。如LCA(6,7)=3,因节点6和节点7 互不为祖先,节点6LCA(6,7)的左子树中,节点7LCA(6,7)的右子树中。

  • 前序遍历中,LCA(S) 出现在所有S 中元素之前,后序遍历中 LCA(S) 则出现在所有 S 中元素之后。这个很好理解。

  • 两点集并的最近公共祖先为两点集分别的最近公共祖先的最近公共祖先,即LCA(A U B )=LCA( LCA(A),LCA(B) )。如下图,点集A={6,7},则LCA(A)=3。点集B={11,12},则LCA(B)=8。则c=A U BLCA(c)=LCA(3,8)=1。利用这个性质,可以求解任意多节点之间的最近公共祖先。

2.png

  • 两点的最近公共祖先必定处在树上两点间的最短路上。如下图,节点97之间的最短路径一定经过其最近公共祖先。这个很好理解,自行参悟。

3.png

  • d(u,v)=h(u)+h(v)-2h(LCA(u,v))。其中 d 是树上两点间的距离,h 代表某点到树根的距离。即,u,v两点之间的距离可以是u到根节点的距离+v到根节点的距离- 减去u,v最近公共祖先到根节点的距离*2。如下图所示,d(6,7)距离。

4.png

2. LCA 朴素算法

知道了什么是LCA后,再来了解怎么得到给定的任意 2 点的最近公共祖先。

向上标记法

向上标记法的思想很简单,如求节点97的最近公共祖先。

5.png

  • 先以节点 9(也可以选择节点7)为起点,向上沿着根节点方向查询,并一路标记访问过的节点,如下图红色节点。

5_1.png

  • 再让节点7向着根节点访问,遇到的第一个标记的节点即为LCA(9,7)=3

5_2.png

同步移位法

  • 首先在树上定位uv的位置。

  • 如果uv的深度不一样,则需要让深度大的节点向上跳跃直到其深度和深度小的节点一致。如查询97两节点的祖先。如下图所示,9的深度为47的深度为3。先移动指向9的指针,让其移动和7深度一致的节点6。然后,同时移动两个指针,直到遇到相同节点3

    Tips: 根节点深度为 1
    6.png

使用矩阵存储树信息,可以很方便写出相应算法。使用邻接表存储树时,为了方便,可以为每一个节点设置一个指向父节点的指针。上述算法可统称为朴素算法,其特点在于算法实现过程中,需要一步一步的移动指针。

本文主要讲解使用培增法求解最近公共祖先。

3. LCA 倍增算法

倍增算法的本质还是补素算法,在其基础上改变了向上跳跃的节奏。不采用一步一步向上跳,而是以2的幂次方向上跳。比如先跳20步、再跳21步……

也就先跳 1步,然后2 步,再然后4 步,再然后8步,再然后16步……

如同前文的同步移位算法思想一样,可先让深度大的节点向上跳,跳到两个节点的深度一致,然后再一起向上跳。

由大到小跳,还是由小到大跳?

由小到大跳,指在移动指针时,先移1位,再移2位,再移 4 位……

下图为由小到大跳的方式实现把指向节点11的指针移到根节点,红色标注为其轨迹点。先 20=1 步到节点8,再21=2步跳到节点 3,下一步再跳时越过根节点,需要在回溯过程中修正。到达根节点,需要跳 3 次。

7.png

如下图是由大到小跳的轨迹点,跳22=4步直接到根节点。

8.png

如上所述,在向上跳跃时,采取由大到小的方案更能提升查询性能。也就是说,在向上跳跃过程中,尽可能一步迈大点。

向上跳几次?

现在继续探讨另一个问题,一个节点向上跳到其父节点,需要跳几次。跳少了肯定是跳不到目标,跳多了会越过目标。比如刚才说,由节点 11跳到根节点1,跳一次就足了。多了无益,少了不能到。

如从节点9跳到根节点,直观而言,可以先跳21=2步。先到达节点3,再跳一步,即跳20步,便到达了根节点。也就是向上跳2次,那么,这个2次是如何得知的?

答案是根据节点到根节点的深度。

如节点11到根节点的深度为 5,一般认定根节点的深度为 1,除掉节点本身的深度,如果采用朴素算法的一步一步向上跳,要向上跳 4次,但是使用倍增法向上跳,因为 22=4,所以理论上跳一次就可以。

如节点9到根节点的深度为 4,除掉本身深度,理论是要跳 3次, 但是3可以拆分成 21+20。倍增法方案可以先跳 2 步,再跳 1 步,2 次可达目标。

所以,要使用倍增算法,先要求解出每一个节点在树上的深度。具体可以使用DFSBFS实现,后文再详细介绍。

缓存节点的祖先:

为了方便找到节点的祖先,可以缓存节点到根节点这条路径上所有的祖先。但是,缓存如下图节点 14的祖先时,并不是把沿着根节点向上所有祖先13、12、8、4、1都缓存下来。

12.png

而是按如下图中的倍增方式缓存,仅缓存了13、12、4几个祖先。

13.png

因每一个节点都需要缓存其祖先信息,显然需要一个二维数组记录这些信息。现设定数组名为 father[i][j]i表示节点的编号,j表示 2 的指数。

father[14][0]表示节点 14的第 20 个祖先,即,father[14][0]=13

father[14][1]表示节点 14的第 21 个祖先,即,father[14][1]=12

father[14][2]表示节点 14的第 22 个祖先,即,father[14][2]=4

……

那么,这些祖先之间有什么样的逻辑关系,先画一个线性图观察一下。

14.png

如上图所示,可得到通过的转移方程式:

  • j=0时,father[i][0]=直接父节点
  • j>0时,father[i][j]= father[ father[i][j-1]][j-1]

其实这个道理也简单,在以2 倍增的表达式中满足:

21=20+20

22=21+21

23=22+22

……

2j=2j-1+2j-1

所以 i 的 2j-1 级祖先的 2j−1 级祖先就是 ij 级祖先。

具体流程:

准备工作到此结束,查询任意 2 个节点的最近公共祖先时,如果 2 个节点的深度不一样,则需要先把 2 个节点深度调整到一样。如下图求解节点514LCA时,需要先把节点14向上移动,找到和节点5深度一样的祖先节点。

15.png

同步深度的流程:

  • 计算节点 14和节点5的深度之差,节点14深度为 6,节点5的深度为3。深度之差为 3
  • 因为 21<3<22 。根据前面缓存信息,跳 22步,即跳到 father[14][2]=4的祖先节点。

16.png

  • 因为节点 4的深度小于节点5的深度,说明跳过头了。需要减少增量,重新以 2步向上跳。跳到节点12位置。

17.png

  • 节点12的深度大于节点5的深度,则设置节点12为新起点,继续向上跳 20=1 步。此时,节点 8和节点5深度相同。

18.png

2 个节点的深度一致,则继续让 2 个节点同时一起向上跳。如下的节点9、10

19.png

向上跳的策略前文说过,从大到小的方向跳。具体实施如下。

  • 两者深度为4,因 4=22。以 j=2向上跳,则到达根节点之外。

20.png

  • 向下减小指数 j的值为 1,重新向上跳 21=2 步。

21.png

  • 直观来讲,节点3LCA,但是程序不能就此做出判断,只能说明找到了公共祖先,但是不能说明是LCA。所以还得修正成向上跳 20=1步。得到 2 个节点的祖先是不相同,此时,可得到结论,节点3LCA

22.png

总结如下:

  • 当跳到的节点是 2 个节点的共同祖先时,则需要再减少指数,重新跳。
  • 当跳到的节点不相同,可以再重新计算深度,继续向上跳。

知道了节点在树上的深度后,如何计算出处于不同深度的节点应该跳多次(也就是 j 指数的取值范围)?

前文举例说明过,如果深度为 3 ,取 3的对数,因 21<3<22。向上取整,即向上跳 2 次,也就是 j 范围为[2,1,0]。可以使用 C++ math库中提供的 lg函数,注意,此函数是以 e为底数,所以需要进行修改。或者自定义lg生成过程。

可以使用预处理lg数组,lg[i]代表深度为i的节点一次性跳多少步可以到达根节点。

for (int i = 1; i <= 10; i++)
    lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);

可以输出lg[0~100]的值。

0 1 2 2 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 

举一个例子,如下图所示,有 2 个深度为 11u和v节点,开始同时向上跳。其过程如下所述:

23.png

  • 根据 lg函数计算深度 11的值,因 23<11<24 ,可得 lg[11]=4。这里的 4 表示 j的值即 2 的指数最大值为 4。 这里可以先跳 24=16,会发现超过根节点,其实这里可以先跳 24-1=8步,先到达如下图所示位置。

24.png

  • 更新u,v的位置到红色节点标记处,然后向上跳 21=2步。到达根节点位置,因相同,再减少指数,跳 20=1步,到达节点 2位置,还是相同,因为已经没有指数可以修改。所以节点 2LCA

编码实现

DFS搜索。

#include <bits/stdc++.h>
using namespace std;
//边
struct Edge {
	int t, nex;
} e[500010 << 1];
//头
int head[500010], tot;

void add(int x, int y) {
	e[++tot].t = y;
	e[tot].nex = head[x];
	head[x] = tot;
}
//记录节点在树上的深度
int depth[500001];
//记录节点的祖先
int father[500001][30];
//存储对数值
int lg[500001];

//now表示当前节点,fa表示它的父亲节点
void dfs(int now, int fa) {
	//记录当前节点的直接父节点
	father[now][0] = fa;
	//当前节点的深度为父节点深度加 1
	depth[now] = depth[fa] + 1;

	//指数范围为 1 ~  lg[depth[now]]
	for(int i = 1; i <= lg[depth[now]]; ++i)
		//动态转移方程式,当前节点的 2^j 祖先是 2^(j-1)祖先的 2^(j-1)祖先
		father[now][i] = father[father[now][i-1]][i-1];
	//递归深度搜索
	for(int i = head[now]; i; i = e[i].nex)
		if(e[i].t != fa) dfs(e[i].t, now);
}

//LCA 求解
int LCA(int x, int y) {
	//不妨设x的深度 >= y的深度
	if(depth[x] < depth[y])
		swap(x, y);
	while(depth[x] > depth[y])
		//先跳到同一深度,注意 depth[x]-depth[y] ] - 1 避免跳过头
		x = father[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)
		if(father[x][k] != father[y][k])
			//因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。
			x = father[x][k], y = father[y][k];
	//返回父节点
	return father[x][0];
}
int main() {
//	freopen("bz.in","r",stdin);
	int n, m, s;
	scanf("%d%d%d", &n, &m, &s);
	for(int i = 1; i <= n-1; ++i) {
		int x, y;
		scanf("%d%d", &x, &y);
		add(x, y);
		add(y, x);
	}
    //自定义 2 为底数的对数计算 
	for(int i = 1; i <= n; ++i)
		lg[i] = lg[i-1] + (1 << lg[i-1] == i);

	dfs(s, 0);
	for(int i = 1; i <= m; ++i) {
		int x, y;
		scanf("%d%d",&x, &y);
		printf("%d\n", LCA(x, y));
	}
	return 0;
}

BFS实现。

const int MAXN=5e4+10;
const int DEG=20;
struct Edge{
    int to,next;
}edge[MAXN<<1];
int head[MAXN],tot;
void addedge(int u,int v){
    edge[tot]=(Edge){v,head[u]};
    head[u]=tot++;
    edge[tot]=(Edge){u,head[v]};
    head[v]=tot++;
}
void init(){
    tot=0;
    memset(head,-1,sizeof(head));
}
int fa[MAXN][DEG];
int dep[MAXN];
void bfs(int r){
    dep[r]=0;
    fa[r][0]=r;
    queue<int> Q;
    Q.push(r);
    while(!Q.empty()){
        int u=Q.front();Q.pop();
        for (int i=1;i<DEG;++i)
            fa[u][i]=fa[fa[u][i-1]][i-1];
        for (int i=head[u];~i;i=edge[i].next) {
            int v=edge[i].to;
            if (v==fa[u][0]) continue;
            dep[v]=dep[u]+1;
            fa[v][0]=u;
            Q.push(v);
        }
    }
}
int LCA(int u,int v){
    if (dep[u]>dep[v]) swap(u,v);
    int hu=dep[u],hv=dep[v];
    int uu=u,vv=v;
    for (int det=hv-hu,i=0;det;det>>=1,++i)
        if(det&1) vv=fa[vv][i];
    if (uu==vv) return uu;
    for (int i=DEG-1;i>=0;--i){
        if (fa[uu][i]==fa[vv][i]) continue;
        uu=fa[uu][i];
        vv=fa[vv][i];
    }
    return fa[uu][0];
}

4. 总结

LCA的求解算法较多,本文详细介绍了倍增算法解决 LCA问题中的细枝末节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1233212.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++入门第八篇---STL模板---list的模拟实现

前言&#xff1a; 有了前面的string和vector两个模板的基础&#xff0c;我们接下来就来模拟实现一下list链表模板&#xff0c;我还是要强调的一点是&#xff0c;我们模拟实现模板的目的是熟练的去使用以及去学习一些对于我们本身学习C有用的知识和用法&#xff0c;而不是单纯的…

35+大龄程序员从焦虑到收入飙升:我的搞钱副业套路分享

37岁大龄程序员&#xff0c;一度觉得自己的职场生涯到头了。既没有晋升和加薪的机会&#xff0c;外面的公司要么接不住我的薪资&#xff0c;要么就是卷得不行&#xff0c;无法兼顾工作和家庭&#xff0c;感觉陷入了死局…… 好在我又重新振作起来&#xff0c;决定用副业和兼职…

Markdown使用emoji图标【美化你的文章】

Markdown使用emoji图标【美化你的文章】 &#x1f308;笔者的文章美化&#xff0c;图标设计 在撰写文章时&#xff0c;使用 Emoji 图标可以为你的文章增添一些趣味和个性化&#xff0c;让它更加吸引眼球&#xff01;✨✨ 首先&#xff0c;Emoji 图标是一种简单而有趣的方式来…

PostgreSQL中所的锁

为了确保复杂的事务可以安全地同时运行&#xff0c;PostgreSQL提供了各种级别的锁来控制对各种数据对象的并发访问&#xff0c;使得对数据库关键部分的更改序列化。事务并发运行&#xff0c;直到它们尝试获取互相冲突的锁为止(比如两个事务更新同一行时)。当多个事务同时在数据…

并行与分布式 第4章 数据级并行:向量体系结构和GPU

文章目录 并行与分布式 第4章 数据级并行&#xff1a;向量体系结构和GPU4.1 什么叫数据级并行4.1.1 数据级并行与SPMD4.1.2数据级并行——传统器件的问题4.1.3 数据级并行——向量体系结构和GPU 4.2 向量体系结构4.2.1 向量以及计算方式4.2.2 向量体系结构4.2.3 向量运算的执行…

腾讯云标准型S5云主机性能评测_CPU内存_带宽系统盘测评

腾讯云服务器CVM标准型S5实例具有稳定的计算性能&#xff0c;CVM 2核2G S5活动优惠价格280.8元一年自带1M带宽&#xff0c;15个月313.2元、2核4G配置748.2元15个月&#xff0c;CPU内存配置还可以选择4核8G、8核16G等配置&#xff0c;公网带宽可选1M、3M、5M或10M&#xff0c;腾…

腾讯云标准型s5和s6有什么区别?CPU处理器有差异吗?

腾讯云服务器CVM标准型S5和S6有什么区别&#xff1f;都是标准型云服务器&#xff0c;标准型S5是次新一代云服务器规格&#xff0c;标准型S6是最新一代的云服务器&#xff0c;S6实例的CPU处理器主频性能要高于S5实例&#xff0c;同CPU内存配置下的标准型S6实例要比S5实例性能更好…

MKRTOS MCU上的微内核操作系统

MKRTOS 全称是 Micro-Kernel Real-Time Operating System&#xff0c;中文名字是微内核实时操作系统。MKRTOS 是首款在开源的支持MCU的微内核操作系统。未来还将在MCU上支持虚拟化&#xff01;&#xff01;下载地址&#xff1a;https://gitee.com/IsYourGod/mkrtos-realMKRTOS被…

深搜回溯剪枝-全排列

LCR 083. 全排列 - 力扣&#xff08;LeetCode&#xff09; 根据题意&#xff0c;要根据给定的整数数组&#xff0c;穷举出所有可能的排列&#xff0c;从直观的角度上来看&#xff0c;可以使用多层 for 循环来解决&#xff0c;但如果是数组长度太大的时候&#xff0c;这种方式不…

老师怎么才能让学生听话

在教育学生的过程中&#xff0c;如何让他们听话并且尊重师长&#xff0c;是一个老师需要深入思考的问题。这不仅涉及到学生的学习进步&#xff0c;还关系到他们的人格形成。以下是一些方法和策略&#xff0c;帮助教师更好地引导学生&#xff0c;使他们更愿意听从教导。 建立信任…

移动机器人路径规划(五)--- 基于Minimun Snap的轨迹优化

目录 1 我们本节主要介绍的 2 Minimum Snap Optimization 2.1 Differential Flatness&#xff08;微分平坦&#xff09; 2 Minimum Snap 3 Closed-form Solution to Minimum Snap 3.1 Decision variable mapping 待优化问题的映射 4 凸优化 及其它问题 1 我们本节主要介…

FL Studio21怎么破解?2024年最新FLStudio21.2.0安装解锁特别版下载使用图文教程

用FL Studio编曲&#xff0c;让音乐成为你的翅膀&#xff0c;飞翔在无尽的创作海洋中吧&#xff01; FL Studio作为一款功能强大且备受赞誉的音乐制作软件&#xff0c;为你提供了一个独特的创作平台。通过FL Studio&#xff0c;你可以自由地创作、编曲&#xff0c;制作属于自己…

利用多核的Rust快速Merkle tree

1. 引言 利用多核的Rust快速Merkle tree&#xff0c;开源代码见&#xff1a; https://github.com/anoushk1234/fast-merkle-tree&#xff08;Rust&#xff09; 其具有如下属性&#xff1a; 可调整为任意高度构建root复杂度为O(n)提供了插入和获取叶子节点的方法获取某叶子节…

『C++成长记』类和对象

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;C &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、类的引入 二、类的定义 三、类的访问限定符 四、类的作用域 五、类的实例化…

给新手教师的成长建议

随着教育的不断发展和进步&#xff0c;越来越多的新人加入到教师这个行列中来。从学生到教师&#xff0c;这是一个华丽的转身&#xff0c;需要我们不断地学习和成长。作为一名新手老师&#xff0c;如何才能快速成长呢&#xff1f;以下是一名老师教师给的几点建议&#xff1a; 一…

腾讯云服务器标准型S5和CVM标准型S6区别对比_选择攻略

腾讯云服务器CVM标准型S5和S6有什么区别&#xff1f;都是标准型云服务器&#xff0c;标准型S5是次新一代云服务器规格&#xff0c;标准型S6是最新一代的云服务器&#xff0c;S6实例的CPU处理器主频性能要高于S5实例&#xff0c;同CPU内存配置下的标准型S6实例要比S5实例性能更好…

2023 羊城杯 final

前言 笔者并未参加此次比赛, 仅仅做刷题记录. 题目难度中等偏下吧, 看你记不记得一些利用手法了. arrary_index_bank 考点: 数组越界 保护: 除了 Canary, 其他保护全开, 题目给了后门 漏洞点: idx/one 为 int64, 是带符号数, 所以这里存在向上越界, 并且 buf 为局部变量,…

谈谈你对mvc和mvvm的理解

MVC和MVVM是软件开发中两种常见的架构模式&#xff0c;各自有不同的优缺点。 MVC&#xff08;Model-View-Controller&#xff09;是一种经典的架构模式&#xff0c;将应用程序分为三个部分&#xff1a;模型&#xff08;Model&#xff09;、视图&#xff08;View&#xff09;和…

buildadmin+tp8表格操作(5)自定义组装搜索的查询

有时候我们会自定义组装一些数据&#xff0c;发送给后端&#xff0c;让后端来进行筛选&#xff0c;这里有一个示例 const onComSearchIdEq () > {// 展开公共搜索baTable.table.showComSearch true/*** 公共搜索表单赋值* 范围搜索有两个输入框&#xff0c;输入框绑定变量…

如何通过数环通,让企业吸引和留住更多优秀人才?

企业招聘员工以及员工入职&#xff0c;不仅仅只是人力资源重要职能之一&#xff0c;它们更是整个企业成功的关键。 市场永远充满竞争&#xff0c;“战争”一直都在&#xff0c;为了赢得胜利&#xff0c;让最优秀的人选加入是最好的选择。但优秀的人才永远不缺机会&#xff0c;市…