LCA(最近公共祖先)

news/2025/2/6 14:28:11/文章来源:https://www.cnblogs.com/blind5883/p/18204065

LCA 就是最近公共祖先,表示为 \(\operatorname{lca}(a, b)\),它的求解方法主要有两种。

倍增法

这是最常用的一种可以动态求 LCA 的算法。时间复杂度为 \(O(\log{n})\)

中心思想

这个算法中有两个特殊的数组:\(depth[i]\)\(fa[i][k]\)
\(depth[i]\)\(i\) 点的深度,以 \(root\)\(0\)
\(fa[i][k]\):从 \(i\) 点向上跳 \(2^k\) 步的点的序号。

对于两个点 \(a,b\),规定 \(depth[a] \ge depth[b]\)
中心思想:我们先找到 \(a\) 上面深度和 \(b\) 相同的点,然后让这两个点同时向上跳,直到两个点刚开始重合,此点就是 \(a,b\) 的最近公共子节点。

对于中心思想里面的向上跳查询深度就是靠 \(depth\)\(fa\) 两个数组实现的。

思路很简单,正确行显而易见,最近重合的点肯定是最近公共祖先。

关于 \(depth\),用 bfs 或者 dfs 可以直接处理出来。
\(fa[i][k]\) 数组,我们有一个方法可以把它递推出来。我们向上跳 \(2^k\) 步,可以看作是,先跳 \(2^{k - 1}\) 步(到 \(fa[i][k - 1]\) 点),然后再跳 \(2^{k-1}\) 步,写成代码即 fa[i][j] = fa[fa[i][k - 1]][k - 1],而这一步也可以用 dfs 或 bfs 处理出来,起始条件为 fa[i][0] = f f 为 i 的父节点。

关于 \(fa[i][k]\)\(k\) 的大小,原则上,只要大于点的数量的 \(\log_2\) 即可。而对于跳过 \(root\) 节点的情况,我们让它是 \(0\) 点,因为默认数组为 \(0\),可以不管它,这样在后面,只要是在 \(0\) 点就可以判断跳过了 \(root\),而且可以方边后面的操作。(跳过了 \(root\) 指向上跳的次数太多,把根节点 \(root\) 都跳过去了)

对于上面的把 \(a\) 跳到和 \(b\) 一个高度,我们 \(fa\) 数组是倍增跳跃的,且可以一次直接跳过根节点,因为任何数都可以变成一个二进制的形式,我们就可以把相差高度按二进制拆开来跳,保证最多 \(O(\log_2n)\) 的时间把 \(a\) 向上跳到和 \(b\) 想同的高度。在实现上,可以直接倒序枚举,\(k\) 让从大的开始,跳不过 \(b\) 的就跳,否则不跳,这样可以保证我们跳完了,\(a,b\) 同一深度。这里不懂可以看代码理解。

预处理时间复杂度 \(O(n\log{n})\),求解 LCA 时间复杂度为 \(O(\log{n})\)

实操步骤

预处理

使用 dfs 或者 bfs 对 \(fa\)\(depth\) 进行预处理。

  1. 进行 dfs,记录两个量,当前点 \(u\)\(u\) 的父节点 \(f\)
  2. 利用 \(f\) 的深度,更新 \(u\) 的深度,即depth[u] = depth[f] + 1;,处理边界条件 fa[u][0] = f ,从 \(1\) 到点数的 \(\log_2\) (设为 \(k\)),即 从 \(1\)\(k\) 枚举,利用转移方程 fa[u][i] = fa[fa[u][i - 1]][i - 1]\(i\) 为枚举的数) 递推出 \(fa\) 数组。
  3. 枚举 u 的子节点,注意判断防止搜回去。
  4. 对每个 dfs 重复 \(2\)\(3\)
动态求解 LCA
  1. 对 a,b 进行求解。
  2. 判断 \(depth\) 大小,使得 \(depth[a] \ge depth[b]\)
  3. 进行向上跳的操作,倒序枚举 \(k\),如果跳后深度不超过 \(depth[b]\) 就进行跳跃,否则不跳
  4. 跳完,\(a,b\) 一定在同一高度,这时候要判断一下 \(a,b\) 是否同一点,如果是,直接输出 \(a\)\(b\)
  5. 进行同时向上跳的操作,倒序枚举 \(k\) ,如果跳后 \(a,b\) 不是同点则继续向上跳,否则不跳。(这里解释一下,这里的原理和第 3 步差不多,但不尽相同。我们设置 \(fa\) 跳过 \(root\) 都是 \(0\),如果跳过了 \(root\),因为相同不跳,所以可以排除这些情况。而对于非跳过 \(root\) 的情况,\(a,b\) 相同说明是 \(a,b\) 的祖宗节点,但是,这不一定是最近的,所以会错误。相反,如果相同的不跳,根据第 \(3\) 步的原理,我最后会跳到一个距离最近祖先最近的非祖先节点,即祖先节点下面一个点,这时候无论是 \(a\) 还是 \(b\) 只要再向上跳一个,就一定是最近公共祖先)。
  6. 向上跳完后,\(a,b\) 任意一个向上跳一步,就是最近公共祖先,即 \(fa[a][0]\) 或者 \(fa[b][0]\)
  7. 输出 \(fa[a][0]\) 或者 \(fa[b][0]\)

代码

显而易见的代码。
预处理推荐 dfs,比较易写不易错

预处理 dfs
void dfs(int u, int f)
{depth[u] = depth[f] + 1;fa[u][0] = f;for (int i = 1; i <= 15; i ++ ) fa[u][i] = fa[fa[u][i - 1]][i - 1];for (int i = h[u]; i != -1; i = ne[i]){int j = e[i];if (j == f) continue;dfs(j, u);}
}
预处理 bfs
void bfs(int root)
{memset(depth, 0x3f, sizeof depth);depth[0] = 0, depth[root] = 1;int tt = 0, hh = 0;q[hh] = root;while (hh <= tt){int t = q[hh ++ ];for (int i = h[t]; i != -1; i = ne[i]){int j = e[i];if (depth[j] > depth[t] + 1){depth[j] = depth[t] + 1;fa[j][0] = t;q[ ++ tt] = j;for (int k = 1; k <= 15; k ++ ) fa[j][k] = fa[fa[j][k - 1]][k - 1];}}}
}
动态求解LCA
int lca(int a, int b)
{if (depth[a] < depth[b]) swap(a, b);for (int k = 15; k >= 0; k -- )if (depth[fa[a][k]] >= depth[b])a = fa[a][k];if (a == b) return a;for (int k = 15; k >= 0; k -- )if (fa[a][k] != fa[b][k]){a = fa[a][k];b = fa[b][k];}return fa[a][0];
}

向上标记法

算是比较劣质的算法,不太可以离线,每次查询最坏时间复杂度为 \(O(n)\)
基本上不用,只给思想。

中心思想

设求 \(a,b\) 的最近公共祖先,
\(a\) 点先不断向上标记,包括它本身,然后从 \(b\) 点向上走,遇到的第一个被标记的点就是它们的最近公共祖先。如下图。

正确性显而易见。

虽然说是 \(O(n)\) ,但是实现起来,空间时间都比较差,大部分情况下动态使用是 \(O(n)\) 的,和Tarjan 的 \(O(n + m)\) 离线不一样。并不优秀。

Tarjan

每错,又双叒叕是 Tarjan。
这是一种离线的做法,时间复杂度为 \(O(n + m)\)\(n\) 是节点数,\(m\) 是询问数
其本质相当于对向上标记法的优化。

中心思想

这里把树上的点分为了三类

  1. 已经被搜完的点(即点所在的函数已经结束了)
  2. 正在被搜的点(即点所在的函数没有结束)
  3. 未搜的点(还没开始搜到)

以下根据图片讲解。
我们能发现一件事,正在被搜的点(下面简称红点)一定成一条链,因为是正在被搜,不是在当前函数中,就是在之前转移来的函数中,因此一条链。所有已经被搜完的点(绿点)和红点都有接触(因为不可能和蓝点有接触),如果把某个绿点 j 归为对应最近的红点 k 之内的话,那个红点 k,就是现在红点 u 和 j 的最大公共祖先。可以把这里的红点看作上面向上标记法中的标记,那么原理就显而易见了。而这样,在 u 时可以求出来,所有绿点和 u 的最大公共祖先。

对于把 j 和 k 合并成一个点,我们只需要用到并查集,而这个搜索是在 dfs 中的,每个点只搜一遍。而我们是离线算法,并查集查询后可以把所有的答案存下来。因此可知查询是 \(O(1)\) 的,而每个点遍历一遍是 \(O(n)\),有 \(m\) 次查询,总的时间复杂度就是 \(O(n + m)\) 的。当然这个时间复杂度也不是太严谨,因为并查集不总是 \(O(1)\) 的时间复杂度。

对于每个点的状态我们分为 \(1,2,0\) 三种,对应,正在搜的点,未搜的点,已经被搜完的点。

实操步骤

  1. 进行 tarjan,设当前点为 \(u\)
  2. \(u\) 的状态设置为 \(1\)
  3. 枚举子节点,把没进行的点进行 tarjan,完成后把子节点和父节点合并成一个集合。
  4. 枚举和 \(u\) 点有关的问题,如果对应点在已经完成点即状态为 \(2\) 的话,查询并查集,记录结果
  5. 循环 \(1\)\(4\)

代码

很简单 awa

void tarjan(int u)
{st[u] = 1;for (int i = h[u]; i != -1; i = ne[i]){int j = e[i];if (st[j]) continue;tarjan(j);p[j] = u;}for (auto itme : query[u]){int y = itme.y, id = itme.id;if (st[y] == 2) res[id] = find(y);}st[u] = 2;
}

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

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

相关文章

CentOS7 图形界面管理(7.9)

1.关于/etc/inittab网上很多都说用着个配置 cat /etc/inittab大概意思就是说,这种设置方式已经不再使用2.建议使用systemctl用法systemctl get-default # multi-user.target #相当于init 3 # graphical.target #相当于init 6 #直接设置 systemctl set-default multi-user.ta…

vue2的混入mixin使用

前言 vue3中不再推荐使用mixins ! 在 Vue 2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue 3 中保留了 mixins 支持,但对于组件间的逻辑复用,使用组合式 API 的组合式函数是现在更推荐的方式。 参考文档:https://juejin.cn/post/7033424132427481101 https://seg…

序列化与反序列化

引用DLL文件:Newtonsoft.Json.dll 路径:D:\yonyou\U9CE\Portal\bin\ C#实体类List<Departments> deptss = new List<Departments>();private void Department_Load(object sender, EventArgs e){strUri = TexURI.Text;strToken = GetToken(TexURI.Text);//Depart…

我的第一台电脑

这篇文章是对我的第一台电脑的回忆,大家感兴趣可以看看。【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://www.cnblogs.com/cnb-yuchen/p/18032053 出自【进步*于辰的博客】大家在看这篇文章的时候,可能会觉得我跑题了。当然,的确有些跑题。而…

Origin2022安装出现“试用期已结束”的解决方法

有小伙伴反馈,在安装Origin2022中文版出现弹窗:试用期已结束。…… 无法继续安装,这个问题该如何解决呢? origin 2022 中文版解决方法: 1、返回电脑桌面,点击左下角【Win标】按钮,在弹出列表中点击【设置】选项 2、点击【时间与语言】3、将【自动设置时间】点选关闭 …

中电金信:华安保险携手中电金信共研共建,重塑财险核心转型新范式

​引言 随着我国数字经济转型深入推进,保险业作为经济“减震器”和社会“稳定器”的作用愈发凸显,国家相继出台一系列发展规划和指导意见引导着保险业数字化转型走深向实。同时围绕人工智能、大数据、云计算等技术产品层出不穷,如智能语音客服、人脸识别、RPA等,推动保险运…

强大的效率工具:电脑桌面待办清单小工具

在这个快节奏的社会,每一分每一秒都显得弥足珍贵。上班族们每天都在与琐碎的工作任务、繁杂的日程计划打交道,如何提高工作效率,让每一天都充实而有条理呢?这时候,一款高效的待办清单工具就显得尤为重要。 那么有没有一款超强大的效率工具呢?电脑桌面待办清单小工具推荐哪…

Kubernetes 数据存储:从理论到实践的全面指南

本文深入解析 Kubernetes (K8S) 数据存储机制,探讨其架构、管理策略及最佳实践。文章详细介绍了 K8S 数据存储的基础、架构组成、存储卷管理技巧,并通过具体案例阐述如何高效、安全地管理数据存储,同时展望了未来技术趋势。关注【TechLeadCloud】,分享互联网架构、云服务技…

BOSHIDA AC/DC电源模块的基本原理与应用

BOSHIDA AC/DC电源模块的基本原理与应用 AC/DC电源模块是一种将交流电转换为直流电的电子设备,它广泛应用于电子设备、电信设备、工控设备以及家电等领域。本文将介绍AC/DC电源模块的基本原理和应用。 AC/DC电源模块的基本原理是通过整流、滤波和稳压等过程将输入的交流电转换…

错误: 找不到或无法加载主类 XXX类 || jmap -histo:live 2345 | less

今天在学习jvm的时候,想要通过 jmap -histo:live 20368 | less 命令 查看堆中存活对象信息。但是在windows系统中貌似好像不支持这个命令 于是我将自己的程序上传到了Linux系统中,但是经过编译完了之后,运行该class文件的时候,提示:错误: 找不到或无法加载主类 XXX类。 这…

关于idea报错提示Output directory is not specified

报错提示:D:\XXX\src\main\java\com\XXX\XXX\base\BaseApiController.java:11:8java: 写入com.XXX.XXX.base.BaseApiController时出错: Output directory is not specified解决措施: 方法一:检查 Maven 配置 确保 pom.xml 中的 Maven 配置正确设置了输出目录。你可以按照以下…

RPC学习记录

RPC是帮助我们屏蔽网络编程细节,实现调用远程方法就跟调用本地方法一样的体验。 RPC是一个远程调用,需要通过网络来传输数据,并且RPC常用于业务系统之间的数据交互,需要保证其可靠性,所以一般默认采用TCP来传输。网络传输数据必须是二进制的,但是调用方请求的出入参数都是…