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

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;//当前节点的深度为父节点深度加 1depth[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.mzph.cn/news/153819.shtml

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

相关文章

linux CentOS7 安装git 配置秘钥公钥克隆代码

第一步&#xff1a;安装git yum -y install git #查看版本 git --version 第二步&#xff1a;配置git信息 git config --global user.name "username" git config --global user.email "XXXXX.com" 第三步&#xff1a;生成密钥和公钥&#xff0c; 后…

裸片-PCBA

裸片 PCBA&#xff0c; 薄膜&#xff0c; 邦定-COB&#xff08;chip on board&#xff09;技术是指将裸芯片直接贴在PCB 板上&#xff0c;然后用铝线或金线进行电子连接的技术

golang 断点调试

1.碰见如下报错,调试器没有打印变量信息 Delve is too old for Go version 1.21.2 (maximum supported version 1.19) 2. 解决办法 升级delve delve是go语言的debug工具。 go install github.com/go-delve/delve/cmd/dlvlatest报错 Get “https://proxy.golang.org/github…

数据驱动-DDT

数据驱动-DDT 说明 自动化测试中&#xff0c;数据驱动是很重要的一个点实际项目中&#xff0c;肯定会出现这种情况&#xff1a;多条测试用例的执行步骤&#xff0c;断言步骤完全一致&#xff0c;只有输入和输出数据不一样这个时候依靠数据驱动&#xff08;数据参数化&#xf…

关于爬虫!看这一篇就够了!

作为一个互联网的技术开发&#xff0c;爬虫不管是自己写的还是所负责的网站被爬&#xff0c;都是挺常见的。 但是一个很常见的东西&#xff0c;却一直没有系统梳理过&#xff0c;今天我们从发展历史&#xff0c;价值&#xff0c;问题和应对恶意爬虫的策略来聊一聊爬虫。 1 爬…

MKRTOS MCU上的微内核操作系统

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

数据仓库:架构之详解Kappa和Lambda

目录 一、前言 二、架构详解 1 Lambda 架构 1.1 Lambda 架构组成 1.2 Lambda 特点 1.3 Lambda 架构的优点 1.4 Lambda 架构的不足 2 Kappa 架构 2.1 Kappa 架构的核心组件 2.2 Kappa 架构优点 2.3 Kappa 架构的注意事项 三、区别对比 四、选择时考虑因素 一、前言 …

Camtasia2024喀秋莎软件注册机

真的要被录屏软件给搞疯了&#xff0c;本来公司说要给新人做个培训视频&#xff0c;想着把视频录屏一下&#xff0c;然后简单的剪辑一下就可以了。可谁知道录屏软件坑这么多&#xff0c;弄来弄去头都秃了&#xff0c;不过在头秃了几天之后&#xff0c;终于让我发现了一个值得“…

企业办公文件数据防泄密系统 | 文件、文档、设计图纸、源代码、音视频等核心数据资料防止外泄!

天锐绿盾防泄密软件采用智能透明加密技术&#xff0c;对文件、文档、图纸、源代码、音视频等数据进行加密保护&#xff0c;防止数据泄露。这种加密技术是内核级透明加密技术&#xff0c;可以在不影响员工正常工作的情况下&#xff0c;对需要保护的数据进行加密操作。 PC端访问地…

小程序授权获取昵称

wxml: <form bindsubmit"formsubmit"><view style"width: 90%;display: flex;margin-left: 5%;"><view class"text1">昵称&#xff1a;</view><input style"width: 150px;margin-left: 30px;margin-top: 30px;…

Harmony Ble 蓝牙App (一)扫描

Harmony Ble 蓝牙App &#xff08;一&#xff09;扫描 前言正文一、创建工程二、工程配置① 权限配置② Debug配置③ UI配置 三、扫描① 扫描接口② 扫描类 四、业务处理① Slice的生命周期② 蓝牙开关和动态权限请求 五、扫描设备六、显示设备① 自定义蓝牙类② 提供者③ 显示…

cp: can‘t stat ‘/usr/share/zoneinfo/Asia/Shanghai‘: No such file or directory

目录 问题描述问题分析解决方案容器时区验证开源项目微服务商城项目前后端分离项目 问题描述 使用下面的 Dockerfile 为 youlai-boot 项目制作镜像设置容器时区报错。 # 基础镜像 FROM openjdk:17-jdk-alpine # 时区修改 RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc…

力扣C++学习笔记——C++ assign全面解析

cassign是一个C20标准中新增的头文件&#xff0c;主要提供了assign函数&#xff0c;用于将一个容器内的元素按照特定规则赋值到另一个容器中。它是STL容器操作的重要一环&#xff0c;具有高效、简洁、易用的特点。 assign函数有多个版本&#xff0c;一般使用的是容器类型相同或…

Vue项目Jenkins自动化部署

1. 需求描述 我们希望提交uat分支时,UAT项目能够自动发布,提交master分支时,无需自动发布,管理员手工发布 2. 效果展示 3. 采用技术 Jenkins + K8S + Docker + Nginx 4. 具体实现 4.1 编写default.conf 在Vue项目根目录新建default.conf文件,主要进行代理配置、首页…

一个模板承包你所有表情包!

深度学习自然语言处理 原创作者&#xff1a;cola meme是一种现代的交流形式&#xff0c;其模板具有基本的语义&#xff0c;任何人都可以在社交媒体上发布它。由于机器学习系统没有足够的上下文来理解meme&#xff0c;因为它比图像和文本有更丰富的内容&#xff0c;所以机器学习…

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

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

EDIFACT学习手册

EDIFACT 又名 UN/EDIFACT&#xff08;全称为 United Nations/Electronic Data Interchange For Administration, Commerce and Transport&#xff09;&#xff0c;是由联合国主导开发制定的国际通用 EDI 标准。EDI术语中的EDIFACT是指 EDIFACT 报文标准&#xff0c;本视频将为大…

虚拟机配置centos7网络

一、编辑虚拟网络 二、编辑 ifcfg-ens32 配置静态ip vim /etc/sysconfig/network-scripts/ifcfg-ens32 三、网卡设置 四、重启网络 systemctl restart network

【外汇天眼】投资之道:成功背后的频繁交易陷阱

成功的投资需要超越人性的短板&#xff0c;其中之一就是频繁交易。巴菲特曾明言&#xff0c;如果商学院的毕业生在毕业后拿一张卡片&#xff0c;每买一支股票就打一个洞&#xff0c;那么这张卡片最终会被打得最少的人将成为巨富。“钱在这里从活跃的投资者流向有耐心的投资者。…

linux内核管理

linux内核会占用一定的空间&#xff0c;所以可以清理一下不需要使用的内核. 参考链接 Linux 内核及其关联文件通常存储在 /boot 目录下&#xff0c;内核模块通常存储在 /lib/modules 目录中。 首先查看已安装的列表&#xff1a; dpkg --list | grep linux-image其中&#xff…