区间信息维护与查询【最近公共祖先LCA 】 - 原理

news2024/9/27 21:20:08

区间信息维护与查询【最近公共祖先LCA 】 - 原理

最近公共祖先(Lowest Common Ancestors,LCA)指有根树中距离两个节点最近的公共祖先。

祖先指从当前节点到树根路径上的所有节点。

在这里插入图片描述

u 和v 的公共祖先指一个节点既是u 的祖先,又是v 的祖先。u 和v 的最近公共祖先指距离u 和v 最近的公共祖先。若v 是u 的祖先,则u 和v 的最近公共祖先是v 。

在这里插入图片描述

可以使用LCA求解树上任意两点之间的距离。求u 和v 之间的距离时,若u 和v 的最近公共祖先为lca,则u 和v 之间的距离为u 到树根的距离加上v 到树根的距离减去2倍的lca到树根的距离:dist[u]+dist[v ]-2×dist[lca]。

在这里插入图片描述

求解LCA的方法有很多,包括暴力搜索法、树上倍增法、在线RMQ算法、离线Tarjan算法和树链剖分。

  • 在线算法: 以序列化方式一个一个地处理输入,也就是说,在开始时并不需要知道所有输入,在解决一个问题后立即输出结果。
  • 离线算法: 在开始时已知问题的所有输入数据,可以一次性回答所有问题。

【原理1】 暴力搜索法

暴力搜索法有两种:向上标记法和同步前进法。

① 向上标记法

从u 向上一直到根节点,标记所有经过的节点;若v 已被标记,则v 节点为LCA(u , v );否则v 也向上走,第1次遇到已标记的节点时,该节点为LCA(u , v )。

在这里插入图片描述

② 同步前进法

将u、v 中较深的节点向上走到和深度较浅的节点同一深度,然后两个点一起向上走,直到走到同一个节点,该节点就是u、v 的最近公共祖先,记作LCA(u , v )。若较深的节点u 到达v 的同一深度时,那个节点正好是v ,则v 节点为LCA(u , v )。

在这里插入图片描述

③ 算法分析

以暴力搜索法求解LCA,两种方法的时间复杂度在最坏情况下均为O(n )。

【原理2】 树上倍增法

树上倍增法不仅可以解决LCA问题,还可以解决很多其他问题,掌握树上倍增法是很有必要的。

F[i , j ]表示i 的2j 辈祖先,即i 节点向根节点走2 j 步到达的节点。

u 节点向上走20 步,则为u 的父节点x ,F[u , 0]=x ;向上走2^1步,到达y ,F[u , 1]=y ;向上走2^2 步,到达z ,F[u , 2]=z ;向上走2^3 步,节点不存在,令F[u , 3]=0。

在这里插入图片描述

F[i , j ]表示i 的2^j 辈祖先,即i 节点向根节点走2^j 步到达的节点。可以分两个步骤:i 节点先向根节点走2^(j -1) 步得到F[i , j-1];再从F[i , j -1]节点出发向根节点走2^(j -1) 步,得到F[F[i , j-1], j -1],该节点为F[i , j ]。

在这里插入图片描述

递推公式:F[i , j ]=F[F[i , j -1], j -1],i =1, 2, …n ,j=0, 1, 2, …k ,2 k ≤n ,k =log2 n 。

① 算法设计

[1] 创建ST。

[2] 利用ST求解LCA。

② 举个栗子

和前面暴力搜索中的同步前进法一样,先让深度大的节点y 向上走到与x 同一深度,然后x、y 一起向上走。和暴力搜索不同的是,向上走是按照倍增思想走的,不是一步一步向上走的,因此速度较快。

问题一: 怎么让深度大的节点y 向上走到与x 同一深度呢?

假设y 的深度比x 的深度大,需要y 向上走到与x 同一深度,k=3,则求解过程如下。

[1] y 向上走2^3 步,到达的节点深度比x 的深度小,什么也不做。

[2] 减少增量,y 向上走2^2 步,此时到达的节点深度比x 的深度大,y 上移,y =F[y ][2]。

在这里插入图片描述

[3] 减少增量,y 向上走2^1 步,此时到达的节点深度与x 的深度相等,y 上移,y =F[y ][1]。

[4] 减少增量,y 向上走2^0 步,到达的节点深度比x 的深度小,什么也不做。此时x、y 在同一深度。

在这里插入图片描述

总结:按照增量递减的方式,到达的节点深度比x 的深度小时,什么也不做;到达的节点深度大于或等于x 的深度时,y 上移,直到增量为0,此时x、y 在同一深度。

问题二: x、y 一起向上走,怎么找最近的公共祖先呢?

假设x 、y 已到达同一深度,现在一起向上走,k =3,则其求解过程如下。

[1] x 、y 同时向上走2^3 步,到达的节点相同,什么也不做。

[2] 减少增量,x 、y 同时向上走2^2 步,此时到达的节点不同,x 、y 上移,x =F[x ][2],y =F[y ][2]。

在这里插入图片描述

[3] 减少增量,x 、y 同时向上走2^1 步,此时到达的节点不同,x 、y 上移,x =F[x ][1],y =F[y ][1]。

[4] 减少增量,x 、y 同时向上走2^0 步,此时到达的节点相同,什么也不做。

此时x 、y 的父节点为最近公共祖先节点,即LCA(x , y )=F[x ][0]。

在这里插入图片描述

完整的求解过程如下图所示。

在这里插入图片描述

总结:按照增量递减的方式,到达的节点相同时,什么也不做;到达的节点不同时,同时上移,直到增量为0。此时x 、y 的父节点为公共祖先节点。

③ 算法实现

void ST_create(){ //构造ST
	for(int j = 1;  j <= k ; j++){
		for(int i = 1; i <= n ; i++){ //i 先走2^(j - 1) 步到达F[i][j - 1] ,再走 2^(j - 1) 步
			F[i][j] = F[F[i][j- 1]][j -1];
		}
	}
}

int LCA_st_query(int x, int y){ // 求x 、 y 的最近公共祖先
	
	if(d[x] > d[y]){ // 保证x 的深度小于或 等于y
		swap(x , y);
	}
	for(int i = k ; i >= 0 ; i --){ //y 向上走到 与 x 同一深度
		if(d[F[y][i]] >= d[x]){
			y = F[y][i];
		}
	}
	if(x == y){
		return x;
	}
	for(int i = k ; i >= 0 ; i --){ // x、y 一起向上走
		if(F[x][i] != F[y][i]){
			x = F[x][i] , y = F[y][i];
		}
	}
	return F[x][0]; //返回x 的父节点
}

④ 算法分析

采用树上倍增法求解LCA,创建ST需要O (n logn )时间,每次查询都需要O (logn )时间。一次建表、多次使用,该算法是基于倍增思想的动态规划,适用于多次查询的情况。若只有几次查询,则预处理需要O (n logn )时间,还不如暴力搜索快。

【原理3】 在线RMQ 算法

两个节点的LCA一定是两个节点之间欧拉序列中深度最小的节点,寻找深度最小值时可以使用RMQ算法。

① 完美图解

欧拉序列指在深度遍历过程中把依次经过的节点记录下来,把回溯时经过的节点也记录下来,一个节点可能被记录多次,相当于从树根开始,一笔画出一个经过所有节点的回路。

在这里插入图片描述

该树的欧拉序列为1 2 4 6 8 6 9 6 4 2 5 7 5 2 1 3 1,搜索时得到6和5首次出现的下标i 、j ,然后查询该区间深度最小的节点,为6和5号节点的最近公共祖先。

在这里插入图片描述

② 算法实现

[1] 深度遍历,得到3个数组:首次出现的下标是pos[],深度遍历得到的欧拉序列是seq[],深度是dep[]。

pos[u] = ++tot; //u 首次出现的下标
seq[tot] = u; //dfs 遍历得到的 欧拉序列
dep[tot] = d; //深度

void dfs(int u , int d){ // dfs 序
	
	vis[u] = true;
	pos[u] = ++tot; //u 首次出现的下标
	
	seq[tot] = u; //dfs 遍历得到的欧拉序列
	dep[tot] = d; //深度
	
	for(int i = head[u] ; i ; i = e[i].next){
		
		int v = e[i].to , w= e[i].c;
		if(vis[v]){
			continue;
		}
		dist[v] = dist[u] + w;
		dfs(v , d + 1);
		seq[++ tot] = u; //dfs 遍历序列
		dep[tot] = d; //深度
	}
}

[2] 根据欧拉序列的深度,创建区间最值查询的ST。F(i , j )表示[i , i +2 j -1]区间深度最小的节点下标。

void ST_create(){ //创建 ST
	for(int i = 1;  i <= tot; i ++){ // 初始化
		F[i][0] = i; //记录下标,不是最小深度
	}
	
	int k = log2(tot);
	for(int j = 1; j <= k ; j ++){
		for(int i = 1; i <= tot - (1 << j) + 1 ; i ++){ // tot - 2 ^ j + 1
			if(dep[F[i][j - 1]] < dep[F[i + (1 << (j - 1))][j - 1]]){
				F[i][j] = F[i][j - 1];					
			}		
			else{
				F[i][j] = F[i + (1 << (j - 1))][j - 1];	
			}
		}
	}
}

[3] 查询[l , r ]区间深度最小的节点下标,与RMQ区间查询类似。

int RMQ_query(int l , int r){ // 查询[ l ,r]  的区间最值
	
	int k  =log2(r -  l + 1);
	if(dep[F[l][k]] < dep[F[r - (1 << k ) + 1][k]]){
		return F[l][k];
	}
	else{
		return F[r - (1 << k) + 1][k]; //返回深度最小的 节点下标
	}
}

[4] 求x、y 的最近公共祖先,先得到x、y 首次出现在欧拉序列中的下标,然后查询该区间深度最小的节点的下标,根据下标读取欧拉序列的节点即可。

int LCA(int x, int y){ // 求x 、 y 的最近公共祖先
	
	int l = pos[x] , r = pos[y]; //读取第 1 次出现的下标
	if(l > r){
		swap(l , r);
	}
	return seq[RMQ_query(l , r)]; //返回节点
}

⑤ 算法分析

在线RMQ算法是基于倍增和RMQ的动态规划算法,其预处理包括深度遍历和创建ST,

需要O (n logn )时间,每次查询都需要O (1)时间。

注意:虽然都用到了ST,但是在线RMQ算法中的ST和树上倍增算法中的ST,其表达的含义是不同的,前者表示区间最值,后者表示向上走的步数。

【原理4】 Tarjan 算法

这里的Tarjan算法是用于解决LCA问题的离线算法

在线算法指每读入一个查询(求一次LCA就叫作一次查询),都需要运行一次程序得到本次查询答案。若一次查询需要 O (logn )时间,则m 次查询需要O (m logn )时间。离线算法指首先读入所有查询,然后运行一次程序得到所有查询答案。Tarjan算法利用并查集优越的时空复杂性,可以在O (n +m )时间内解决LCA问题。

① Tarjan 算法

[1] 初始化集合号数组和访问数组,fa[i ]=i ,vis[i ]=0。

[2] 从u 出发深度优先遍历,标记vis[u ]=1,深度优先遍历u 所有未被访问的邻接点,在遍历过程中更新距离,回退时更新集合号。

[3] 当u 的邻接点全部遍历完毕时,检查关于u 的所有查询,若存在一个查询u 、v ,而vis[v ]=1,则利用并查集查找v 的祖宗,找到的节点就是u 、v 的最近公共祖先。

② 举个栗子

在树中求5、6的最近公共祖先,求解过程如下。

[1] 初始化所有节点的集合号都等于自己,fa[i ]=i ,vis[i]=0。

在这里插入图片描述

[2] 从根节点开始深度优先遍历,在遍历过程中标记vis[]=1。

在这里插入图片描述

[3] 8号节点的邻接点已访问完毕,更新fa[8]=6,没有8相关的查询,回退到6。

在这里插入图片描述

[4] 遍历6号节点的下一个邻接点9,标记vis[9]=1,9号节点的邻接点已访问完毕,更新fa[9]=6,没有9相关的查询,回退到6。

在这里插入图片描述

[5] 6号节点的邻接点已访问完毕,更新fa[6]=4,有6相关的查询5(查询5 6),但是vis[5]≠1,什么也不做,返回到4。

在这里插入图片描述

[6] 4号节点的邻接点已访问完毕,更新fa[4]=2,没有4相关的查询,返回到2。

在这里插入图片描述

[7] 遍历2号节点的下一个邻接点5,标记vis[5]=1,继续深度遍历到7,标记vis[7]=1,7号节点的邻接点已访问完毕,更新fa[7]=5,没有7相关的查询,回退到5。

在这里插入图片描述

[8] 5号节点的邻接点已访问完毕,更新fa[5]=2,有5相关的查询6(查询5 6),且vis[6]=1,此时需要从6号节点开始使用并查集查找祖宗。

在这里插入图片描述

[9] 从6号节点开始利用并查集查找祖宗的的过程如下。首先判断6的集合号fa[6]=4,找4的集合号fa[4]=2,找2的集合号fa[2]=2,找到祖宗(集合号为其自身)后返回,并更新祖宗到当前节点路径上所有节点的集合号,即更新6、4的父节点fa[4]=2,fa[6]=2,此时fa[6]就是5和6的最近公共祖先。

在这里插入图片描述

[总结]

在当前节点u 的邻接点已访问完毕时,检查u 相关的所有查询v ,若vis[v ]≠1,则什么也不做;若vis[v ]=1,则利用并查集查找v 的祖宗,lca(u , v )=fa[v ]。实际上,u 的祖宗就是u 向上查找第1个邻接点未访问完的节点,它的fa[]还没有更新,仍满足fa[i]=i ,它就是v 的祖宗。

③ 算法实现

int find(int x){ //并查集 找祖宗
	if(x != fa[x]){
		fa[x] = find(fa[x]); 
	}
	return fa[x];
}

void tarjan(int u){ // Tarjan算法
	
	vis[u] = 1;
	for(int i = head[u] ; i ; i = e[i].next){
		int v = e[i].to , w = e[i].c;
		if(vis[v]){
			continue;
		}
		dis[v] = dis[u] + w;
		tarjan(v);
		fa[v] = u;
	}
	for(int i = 0; i < query[u].size() ; i ++){ // u相关的所有查询
		int v = query[u][i];
		int id = query_id[u][i];
		
		if(vis[y]){
			int lca = find(v);
			ans[id] = dis[u] + dis[v] - 2 * dis[lca];
		}
	}
}

④ 算法分析

离线Tarjan算法用到了并查集的优越性,m 次查询的时间为O (n+m )。

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

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

相关文章

React基础之Context

前文有讲到Vue中的provide透传&#xff0c;祖孙组件之间通信。在react中也有类似的存在&#xff0c;他就是context&#xff0c;context的作用也是让祖孙组件之前通信。 React中&#xff0c;通过createContext方法来创建一个Context对象。 import React, { createContext } fr…

sqli-labs/Less-51

这一关的欢迎界面依然是以sort作为注入点 我们首先来判断一下是否为数字型注入 输入如下 sortrand() 对尝试几次 发现页面并没有发生变化 说明这道题的注入类型属于字符型 然后尝试输入以下内容 sort1 报错了 报错信息如下 我们从报错信息可以知道这道题的注入类型属于单…

JS-基础

JavaScript 基础第一天 01 JavaScript介绍 1.1 JavaScript 注释 单行注释 符号&#xff1a;//作用&#xff1a;//右边这一行的代码会被忽略快捷键&#xff1a;ctrl / 块注释 符号&#xff1a;/* */作用&#xff1a;在/* 和 */ 之间的所有内容都会被忽略快捷键&#xff1a;s…

【附源码】计算机毕业设计JAVA移动在线点菜系统服务端服务端

【附源码】计算机毕业设计JAVA移动在线点菜系统服务端服务端 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1…

【Unity】TimeLine系列教程——编排剧情!

前言 我们经常会看到游戏中有很多“花里胡哨”的系统&#xff0c;比如这样&#xff1a; 火影忍着疾风传 碧之轨迹S技能 这种效果感觉上像播放视频一样&#xff0c;但是却能将游戏不同的敌人加到镜头里面&#xff0c;有时候甚至根据双方关系还会有不同的反馈。如果是做视频或…

基于DQN的强化学习 快速浏览(基础知识+示例代码)

一、强化学习的基础概念 强化学习中有2个主要的实体&#xff0c;一个是智能体(agent)&#xff0c;另一个是环境(environment)。在强化学习过程中&#xff0c;智能体能够得到的是环境当前的状态(State)&#xff0c;即环境智能体所处环境当前的情况。另一个是上一步获得的环境的…

NePTuNe 论文笔记

NePTuNe:Neural Powered Tucker Network for Knowledge Graph Completion- Introduction- Background- Algorithm- Experiment- Conclusion- CodeShashank Sonkar, Arzoo Katiyar, Richard G.Baraniuk - Introduction 目前的链接预测方法&#xff1a; 张量因式分解&#xff1…

【服务器数据恢复】raidz多块硬盘离线的数据恢复案例

服务器数据恢复环境&#xff1a; 一台采用zfs文件系统的服务器&#xff0c;配备32块硬盘。 服务器故障&#xff1a; 服务器在运行过程中崩溃&#xff0c;经过初步检测没有发现服务器有物理故障&#xff0c;重启服务器后故障依旧&#xff0c;用户联系我们中心要求恢复服务器数据…

SpringBoot: Controller层的优雅实现

目录1. 实现目标2. 统一状态码3. 统一响应体4. 统一异常5. 统一入参校验6. 统一返回结果7. 统一异常处理8. 验证1. 实现目标 优雅校验接口入参响应体格式统一处理异常统一处理 2. 统一状态码 创建状态码接口&#xff0c;所有状态码必须实现这个接口&#xff0c;统一标准 pa…

Eolink 征文活动- -专为开发者设计的一款国产免费 API 协作平台

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; ▌背景 后端开发的程序员都需要有一个用得顺手的接口测试工具&#xff1b;以前&#xff0c;大家都喜欢用Google开发的一款接口测试工具postman来进行测试&#xff0c;…

Java面向对象三大基本特征之多态

多态性是面向对象编程的又一个重要特征&#xff0c;那么多态是什么呢&#xff1f; 一、多态的概念 1.概念&#xff1a;多态是指在父类中定义的属性和方法被子类继承之后&#xff0c;可以具有不同的数据类型或表现出不同的行为&#xff0c;这使得同一个属性或方法在父类及其各…

Linux 文件操作(一) —— 遍历指定目录下的所有文件

目录 一、访问目录相关函数 1、打开/访问目录 (opendir / fdopendir) 2、读取目录内容 (readdir) 3、关闭目录 (closedir) 二、遍历指定目录下的所有文件 一、访问目录相关函数 1、打开/访问目录 (opendir / fdopendir) opendir / fdopendir 函数的作用是访问指定路径的…

工程基建--前端基建

序&#xff1a; 工程基建 &#xff1a; 编码规范、api规范、前后端协作、环境部署、微服务、微前端、性能、安全防御、统计监控、可视化 等等的建设&#xff1b; 后端基建&#xff1a; 后端规范文档、后端模板、安全、日志、微服务、RESTful API、中间件、数据库、分布式、权…

新手怎么做微信商城小程序_微信商城小程序模版哪里找

微信小程序已经在我们的生活中随处可见&#xff0c;甚至是抖音头条等其它的平台也开始做起了小程序&#xff0c;在这种情况下&#xff0c;微信小程序势必会成为未来商城的主战场之一。闻风而来想做小程序的人不少&#xff0c;而其中新手零基础也能做的小程序商城模板类工具&…

C++入门教程2||C++ 数据类型

C 数据类型 使用编程语言进行编程时&#xff0c;需要用到各种变量来存储各种信息。变量保留的是它所存储的值的内存位置。这意味着&#xff0c;当您创建一个变量时&#xff0c;就会在内存中保留一些空间。 您可能需要存储各种数据类型&#xff08;比如字符型、宽字符型、整型…

【Leetcode】15. 三数之和

一、题目 难度不小 注意是不能重复 Python提交格式&#xff0c;返回一个list 二、暴力解法 排序 三重循环 有没有像我这样的傻子&#xff0c;三重循环&#xff0c;还没去重 后来发现要去重&#xff0c;必须要先排序&#xff0c;然后判断一下当前的数是否跟前面那个数相同&am…

SpringBoot项目如何优雅的实现操作日志记录

前言 大家好&#xff0c;我是希留。 在实际开发当中&#xff0c;对于某些关键业务&#xff0c;我们通常需要记录该操作的内容&#xff0c;一个操作调一次记录方法&#xff0c;每次还得去收集参数等等&#xff0c;会造成大量代码重复。 我们希望代码中只有业务相关的操作&…

html5期末大作业 基于HTML+CSS制作dr钻戒官网5个页面 企业网站制作

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Java中的多重继承问题

继承是面向对象编程 &#xff08;OOP&#xff09; 语言&#xff08;如Java&#xff09;的主要功能之一。它是一种以增强软件设计中类重用能力的方式组织类的基本技术。多重继承是众多继承类型之一&#xff0c;是继承机制的重要原则。但是&#xff0c;它因在类之间建立模棱两可的…

使用HTML实现一个静态页面(含源码)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…