【算法笔记】最近公共祖先(LCA)算法详解

news2025/1/15 6:29:04

0. 前言

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

这种算法应用很广泛,可以很容易解决树上最短路等问题。

为了方便,我们记某点集 S = { v 1 , v 2 , … , v n } S=\{v_1,v_2,\ldots,v_n\} S={v1,v2,,vn} 的最近公共祖先为 LCA ( v 1 , v 2 , … , v n ) \text{LCA}(v_1,v_2,\ldots,v_n) LCA(v1,v2,,vn) LCA ( S ) \text{LCA}(S) LCA(S)

部分内容参考 OI Wiki,文章中所有算法均使用C++实现。

例题:洛谷 P3379 【模板】最近公共祖先(LCA)

1. 性质

  1. LCA ( { u } ) = u \text{LCA}(\{u\})=u LCA({u})=u
  2. u u u v v v 的祖先,当且仅当 LCA ( u , v ) = u \text{LCA}(u,v)=u LCA(u,v)=u
  3. 如果 u u u 不为 v v v 的祖先并且 v v v 不为 u u u 的祖先,那么 u , v u,v u,v 分别处于 LCA ( u , v ) \text{LCA}(u,v) LCA(u,v) 的两棵不同子树中;
  4. 前序遍历中, LCA ( S ) \text{LCA}(S) LCA(S) 出现在所有 S S S 中元素之前,后序遍历中 LCA ( S ) \text{LCA}(S) LCA(S) 则出现在所有 S S S 中元素之后;
  5. 两点集并的最近公共祖先为两点集分别的最近公共祖先的最近公共祖先,即 LCA ( A ∪ B ) = LCA ( LCA ( A ) , LCA ( B ) ) \text{LCA}(A\cup B)=\text{LCA}(\text{LCA}(A), \text{LCA}(B)) LCA(AB)=LCA(LCA(A),LCA(B))
  6. 两点的最近公共祖先必定处在树上两点间的最短路上;
  7. d ( u , v ) = h ( u ) + h ( v ) − 2 h ( LCA ( u , v ) ) d(u,v)=h(u)+h(v)-2h(\text{LCA}(u,v)) d(u,v)=h(u)+h(v)2h(LCA(u,v)),其中 d d d 是树上两点间的距离, h h h 代表某点到树根的距离。

2. 求解算法

2.0 前置知识1:树的邻接表存储

简单来说,树的邻接表存储就是对于每个结点,存储其能通过一条有向或无向边,直接到达的所有结点。
传统的存储方式是使用链表(或模拟链表),这样实现比较麻烦,也容易写错。
此处为了更好的可读性我们使用STL中的可变长度顺序表vector

#include <vector> // 需要使用STL中的vector
#define maxn 100005 // 最大结点个数

std::vector<int> G[maxn];

此时,若要添加一条无向边 u ↔ v u\leftrightarrow v uv,可使用:

G[u].push_back(v);
G[v].push_back(u);

若要添加 u → v u\to v uv的有向边:

G[u].push_back(v);

遍历 v v v能直接到达的所有结点:

for(int u: G[v])
	cout << u << endl;

2.1 前置知识2:DFS 遍历 & 结点的深度计算

对于两种算法,都需要预处理出每个结点的深度。
一个结点的深度定义为这个结点到树根的距离。

要预处理出所有结点的深度,很简单:
运用树形dp的方法,令 h u h_u hu 表示结点 u u u 的深度,逐层向下推进:

#include <cstdio>
#include <vector>
#define maxn 100005
using namespace std;

vector<int> G[maxn]; // 邻接表存储
int depth[maxn]; // 每个结点的深度

void dfs(int v, int par) // dfs(当前结点,父亲结点)
{
	int d = depth[v] + 1; // 子结点的深度=当前结点的深度+1
	for(int u: G[v])
		if(u != par) // 不加这条判断会无限递归
		{
			depth[u] = d; // dp更新子结点深度
			dfs(u, v); // 往下dfs
		}
}

int main()
{
	// 构建一张图
	// ...
	// 假定图已存入邻接表G:
	int root = 0; // 默认树根为0号结点,根据实际情况设置
	dfs(root, -1); // 对于根结点,父亲结点为-1即为无父亲结点
	return 0;
}

2.1 朴素算法

u , v u,v u,v 表示两个待求 LCA 的结点。需提前预处理出每个结点的父亲(记结点 v v v 的父亲为 f v f_v fv)。

算法步骤:

  1. 使 u , v u,v u,v 的深度相同:可以让深度大的结点往上走,直到与深度小的结点深度相同。
  2. u ≠ v u\ne v u=v时: u ← f u , v ← f v u\gets f_u,v\gets f_v ufu,vfv
  3. 循环直到 u = v u=v u=v,此条件成立后 u u u v v v 的值即为我们要求的 LCA。

时间复杂度分析:

  • 预处理:DFS 遍历整棵树, O ( N ) \mathcal O(N) O(N)
  • 单次查询:最坏 O ( N ) \mathcal O(N) O(N),平均 O ( log ⁡ N ) \mathcal O(\log N) O(logN)(随机树的高为 ⌈ log ⁡ N ⌉ \lceil\log N\rceil logN

参考代码:

#include <cstdio>
#include <vector>
#include <algorithm>
#define maxn 500005
using namespace std;

vector<int> G[maxn];
int depth[maxn], par[maxn];

void dfs(int v)
{
	int d = depth[v] + 1;
	for(int u: G[v])
		if(u != par[v])
		{
			par[u] = v, depth[u] = d;
			dfs(u);
		}
}

int lca(int u, int v)
{
	if(depth[u] < depth[v])
		u ^= v ^= u ^= v;
	while(depth[u] > depth[v])
		u = par[u];
	while(u != v)
		u = par[u], v = par[v];
	return u;
}

int main()
{
	int n, q, root;
	scanf("%d%d%d", &n, &q, &root);
	for(int i=1; i<n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	par[root] = -1, depth[root] = 0;
	dfs(root);
	while(q--)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		printf("%d\n", lca(u, v));
	}
	return 0;
}

可以发现,程序在最后四个测试点上TLE了:

TLE

这是因为,这四个点是专门针对朴素算法设计的(正好是一个 Subtask),使算法的时间复杂度达到了最坏情况 O ( N Q ) \mathcal O(NQ) O(NQ),而 N , Q ≤ 5 × 1 0 5 N,Q\le 5\times 10^5 N,Q5×105,所以无法通过测试点。当然,朴素算法在随机树上回答 Q Q Q 次询问的时间复杂度还是 O ( N + Q log ⁡ N ) \mathcal O(N+Q\log N) O(N+QlogN),被极端数据卡掉也没办法

2.2 倍增

倍增算法是朴素算法的改进算法,也是最经典的 LCA 求法。

预处理:

  • fa x , i \text{fa}_{x,i} fax,i 表示点 x x x 的第 2 i 2^i 2i 个祖先。
  • dfs 预处理深度信息时,也可以预处理出 fa x , i \text{fa}_{x,i} fax,i
    • 首先考虑 i i i的范围: 2 i ≤ d x 2^i\le d_x 2idx(前面说的, d x d_x dx 表示结点 x x x 的深度),所以有 0 ≤ i ≤ ⌊ log ⁡ 2 d x ⌋ 0\le i\le \lfloor\log_2 d_x\rfloor 0ilog2dx
    • 对于 i = 0 i=0 i=0 2 i = 2 0 = 1 2^i=2^0=1 2i=20=1,所以直接令 fa x , 0 = ( x 的父亲 ) \text{fa}_{x,0}=(x\text{的父亲}) fax,0=(x的父亲) 即可。
    • 对于 1 ≤ i ≤ ⌊ log ⁡ 2 d x ⌋ 1\le i\le \lfloor\log_2 d_x\rfloor 1ilog2dx x x x 的第 2 i 2^i 2i 个祖先可看作 x x x 的第 2 i − 1 2^{i-1} 2i1 个祖先的第 2 i − 1 2^{i-1} 2i1 个祖先( 2 i − 1 + 2 i − 1 = 2 i 2^{i-1}+2^{i-1}=2^i 2i1+2i1=2i),即:
      fa x , i = fa fa x , i − 1 , i − 1 \text{fa}_{x,i}=\text{fa}_{\text{fa}_{x,i-1},i-1} fax,i=fafax,i1,i1

求解步骤:

  1. 使 u , v u,v u,v 的深度相同:计算出 u , v u,v u,v 两点的深度之差,设其为 y y y。通过将 y y y 进行二进制拆分,我们将 y y y 次游标跳转优化为「 y y y 的二进制表示所含 1 的个数」次游标跳转(详见代码)。
  2. 特判:如果此时 u = v u=v u=v,直接返回 u u u v v v 作为 LCA 结果。
  3. 同时上移 u u u v v v:从 i = ⌊ log ⁡ 2 d u ⌋ i=\lfloor\log_2 d_u\rfloor i=log2du 开始循环尝试,一直尝试到 0 0 0(包括 0 0 0),如果 fa u , i ≠ fa v , i \text{fa}_{u,i}\not=\text{fa}_{v,i} fau,i=fav,i,则 u ← fa u , i , v ← fa v , i u\gets\text{fa}_{u,i},v\gets\text{fa}_{v,i} ufau,i,vfav,i,那么最后的 LCA 为 fa u , 0 \text{fa}_{u,0} fau,0

时间复杂度分析:

  • 预处理: O ( N ) \mathcal O(N) O(N) DFS ×   O ( log ⁡ N ) \times~\mathcal O(\log N) × O(logN) 预处理 = O ( N log ⁡ N ) =\mathcal O(N\log N) =O(NlogN)
  • 单次查询:平均 O ( log ⁡ n ) O(\log n) O(logn),最坏 O ( log ⁡ n ) O(\log n) O(logn)
  • 预处理+ Q Q Q次查询: O ( N + Q log ⁡ N ) \mathcal O(N+Q\log N) O(N+QlogN)

另外倍增算法可以通过交换 fa 数组的两维使较小维放在前面。这样可以减少 cache miss 次数,提高程序效率。

参考代码:

#include <cstdio>
#include <vector>
#include <cmath>
#define maxn 500005
using namespace std;

vector<int> G[maxn];
int fa[maxn][19]; // 2^19=524288
int depth[maxn];

void dfs(int v, int par)
{
	fa[v][0] = par;
	int d = depth[v] + 1;
	for(int i=1; (1<<i)<d; i++)
		fa[v][i] = fa[fa[v][i - 1]][i - 1];
	for(int u: G[v])
		if(u != par)
			depth[u] = d, dfs(u, v);
}

inline int lca(int u, int v)
{
	if(depth[u] < depth[v])
		u ^= v ^= u ^= v;
	int m = depth[u] - depth[v];
	for(int i=0; m; i++, m>>=1)
		if(m & 1)
			u = fa[u][i];
	if(u == v) return u; // 这句不能丢
	for(int i=log2(depth[u]); i>=0; i--)
		if(fa[u][i] != fa[v][i])
			u = fa[u][i], v = fa[v][i];
	return fa[u][0];
}

int main()
{
	int n, q, root;
	scanf("%d%d%d", &n, &q, &root);
	for(int i=1; i<n; i++)
	{
		int x, y;
		scanf("%d%d", &x, &y);
		G[--x].push_back(--y);
		G[y].push_back(x);
	}
	depth[--root] = 0;
	dfs(root, -1);
	while(q--)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		printf("%d\n", lca(--u, --v) + 1);
	}
	return 0;
}

AC

3. 习题

  • 题目链接:洛谷 P8805 [蓝桥杯 2022 国 B] 机房
  • 题解:https://best-blogs.blog.luogu.org/solution-p8805

4. 总结

本文详细讲解了 LCA 问题以及求解 LCA 的两种算法。

if(hasSanLian())
	cout << "Thank you!";
if hasSanLian():
	print('Thank you!')

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

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

相关文章

企业内训方案|领导力与执行力/TTT内训师/管理者情商修炼

企业内训方案|领导力与执行力/TTT内训师/管理者情商修炼 》》领导力与执行力 从精兵到强将 高绩效团队协作与跨部门沟通 核心人才的管理与激励 卓越管理者的胜任力提升 MTP中层管理技能提升训练 打造高绩效团队 高效沟通技巧 高绩效团队管理&#xff08;中高层/中基层&#xf…

CRM帮助企业实现销售自动化

随着互联网技术的发展&#xff0c;各家企业都善用互联网优势发布各种信息&#xff0c;导致潜在客户被各种推销信息所淹没&#xff0c;销售周期延长&#xff0c;企业可以借助CRM有效规范销售流程&#xff0c;帮助企业实现销售自动化。 前言 各行各业的业务流程中似乎都少不了销…

OSPF综合实验(1.5)

目标&#xff1a; 1、首先进行基于172.16.0.0/16的ip地址规划 首先题中有5个区域和一个RIP共需要5个网段 可以借3位划分为8个网段 172.16.0.0/19 area 0 然后将172.16.0.0/19再借6位分为172.16.0.0/25---172.16.31.128 25作为其中前一个骨干ip网段 172.16.0.0/25在用于只…

TCP滑动窗口机制(附图例)

文章目录前言一、滑动窗口的引出二、流量控制2.1 16位窗口大小2.2 发送缓冲区2.3 逐步解析滑动窗口运作三、快重传机制四、拥塞控制&#xff08;仅供参考&#xff09;五、延迟应答与捎带应答&#xff08;略&#xff09;总结前言 博主个人社区&#xff1a;开发与算法学习社区 博…

测开-刷笔试题时的知识点

圈复杂度&#xff08;暂缓&#xff09;复杂度越大&#xff0c;程序越复杂计算公式&#xff1a;V(G) E - N 2E代表控制流边的数量&#xff0c;n代表节点数量V (G) P 1p为判定节点数几种常见的控制流图&#xff1a;Linux文件权限具有四种访问权限&#xff1a;r&#xff08;可…

进程信号理解3

进程信号理解3 1.什么叫做信号递达 实际执行信号的处理动作叫做信号递达&#xff0c;比如默认&#xff0c;忽略&#xff0c;自定义动作 2.什么叫做信号未决&#xff1f; 信号产生到信号递达的状态叫做信号未决 3.进程被阻塞和进程被忽略有什么区别&#xff1f; 进程被阻塞属…

iPhone更换字体教程,无需越狱,支持所有苹果设备!

上周开始&#xff0c;技术大神zhuowei 发现了一个iOS系统更换字体的漏洞&#xff0c;经过不断修正&#xff0c;现在已经可利用上了&#xff01; 先来看看更换字体后的效果&#xff0c;更换之后&#xff0c;所有App上的字体都得到更改&#xff0c;下图是打开文章的效果 下图是聊…

excel查重技巧:如何用组合函数快速统计重复数据(上)

统计不重复数据的个数&#xff0c;相信不少小伙伴在工作中都遇到过这样的问题。通常的做法都是先把不重复的数据提取出来&#xff0c;再去统计个数。而提取不重复数据的方法之前也分享过&#xff0c;基本有三种方法&#xff1a;高级筛选、数据透视表和删除重复项。其实使用公式…

Ngnix 实现访问黑名单功能

前言 有时候在配置的时候我们会禁用到一些IP&#xff0c;使用nginx 禁用到ip但是需要重启nginx&#xff0c;这样当我们要是实现动态的这种就比较麻烦&#xff0c;当然你可以使用网关来实现相对于nginx实现的这种方式要好很多&#xff0c;但是今天咱们说到这里&#xff0c;那就…

数据可视化系列-05数据分析报告

文章目录数据可视化系列-05数据分析报告1、了解初识数据分析报告数据分析报告简介数据分析报告的作用报告的能力体现报告编写的原则报告种类2、掌握数据分析报告结构标题页目录前言正文结论与建议附录3、了解报告的描述规范报告注意事项报告表达的维度数据结论可用指标数据可视…

代码随想录算法训练营第3天| 203. 移除链表元素、206. 反转链表

代码随想录算法训练营第3天| 203. 移除链表元素、206. 反转链表 移除链表元素 力扣题目链接 删除链表中等于给定值 val 的所有节点。 这里以链表 1 4 2 4 来举例&#xff0c;移除元素4。 那么因为单链表的特殊性&#xff0c;只能指向下一个节点&#xff0c;刚刚删除的是链表…

RS485通信----基本原理+电路图

一、RS485 通信----简介 RS485 是美国电子工业协会&#xff08;Electronic Industries Association&#xff0c;EIA&#xff09;于1983年发布的串行通信接口标准&#xff0c;经通讯工业协会&#xff08;TIA&#xff09;修订后命名为 TIA/EIA-485-A。 RS485 是一种工业控制环境…

获取Java集合中泛型的Class对象

直接获取时获取不到的&#xff0c;类型被虚拟机擦除了 泛型的正常工作是依赖编译器在编译源码的时候&#xff0c;先进行类型检查&#xff0c;然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。编译器在编译时擦除了所有类型相关的信息&#xff0c;所以…

【36张图,一次性补全网络基础知识】

OSI和TCP/IP是很基础但又非常重要的知识&#xff0c;很多知识点都是以它们为基础去串联的&#xff0c;作为底层&#xff0c;掌握得越透彻&#xff0c;理解上层时会越顺畅。今天这篇网络基础科普&#xff0c;就是根据OSI层级去逐一展开的。 01 计算机网络基础 01 计算机网络的…

让阿里再次伟大--钉钉如何长成独角兽的?

文章目录引子开端发展历程&#xff1a;从2014到2022钉钉和阿里云的全面融合钉钉体系架构技术挑战ToB与ToC的差异安全要求高稳定性要求高业务多样性钉钉的创新存储创新单元化平台开放这些年&#xff0c;钉钉做了哪些优化?钉钉的技术栈钉钉的竞争对手们飞书微信华为welink未来参…

ArcGIS基础实验操作100例--实验58二维点、线转三维

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验58 二维点、线转三维 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&…

二、kubernetes集群环境搭建

文章目录1.前置知识点2.kubeadm 部署方式介绍3.安装要求4.最终目标5.集群环境6 初始化环境6.1 检查操作系统版本6.2 主机名解析6.3 时间同步6.4 禁用selinux6.5 禁用swap分区6.6 修改linux的内核参数6.7 配置ipvs功能6.8 安装docker6.9 安装Kubernetes组件6.10 准备集群镜像6.1…

多线程案例

日升时奋斗&#xff0c;日落时自省 目录 1、单例模式 1.1、饿汉模式 1.2、懒汉模式 1.3、饿汉和懒汉的线程安全 2、生产者消费者模型 2.1、理论解释 2.2、优势 2.3、阻塞队列代码解析 2.4、生产者消费者代码解析 2.5、简单实现阻塞队列代码解析 3、定时器 3.1、定时…

Java中String、StringBuffer 和 StringBuilder 的区别

1. String 字符串常量&#xff0c;字符串长度不可变。Java 中 String 是 immutable&#xff08;不可变&#xff09;的。 2. StringBuffer 1.如果要频繁对字符串内容进行修改&#xff0c;出于效率考虑最好使用 StringBuffer&#xff0c;如果想转成 String 类型&#xff0c;可以调…

论文笔记Neural Ordinary Differential Equations

论文笔记Neural Ordinary Differential Equations概述参数的优化连续标准化流&#xff08;Continuous Normalizing Flows&#xff09;生成式的隐轨迹时序模型&#xff08;A generative latent function time-series model&#xff09;这篇文章有多个版本&#xff0c;在最初的版…