RMQ问题的ST算法实现(详细解析+图片演示+ST模板代码)

news2025/1/10 16:04:42

文章目录

  • RMQ问题
    • 问题引入
    • ST算法
      • 倍增
      • ST递推公式
      • 查询任意区间的最值
    • 代码实现

RMQ问题

RMQ(Range Minimum/Maximum Query)问题,又叫做区间最值问题,即对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值。

问题引入

给你一个序列: [4,6,9,8,7,6,3,1,2,4],输入一个left和一个right,表示在 [left,right]这段这个范围的最值是多少?我们假定数组的下标从1开始,数组的下标范围:[1,10]
例如:
left=1 right=5 : [4,6,9,8,7] 最大值是 9,最小值是4
left=4,right=6 : [8,7,6] 最大值是8,最小值是6

我们如何求解这样的问题呢?

暴力解法容易想出来,我们只需要 打擂台算法便可以求出这个区间的最值,打擂台算法指的是从区间的开始下标处取得 nums[left]当作第一个元素,记作num,然后依次与[left, right]内的元素进行比较,要得到最大值,则当后面的元素比num大时,更新num;要取得最小值则反之。

for (int i=left;i<right;i++) //假定left,right满足下标范围
{
	//取最大值
	if (numMax<nums[i]) numMax=nums[i];
	else if (numMin>nums[i]) numMin=nums[i];
}

这个算法,我们每次都需要枚举每一个范围内的元素,进而更新得到合适的值,但是它的时间复杂度呢?
假设我们需要 询问m次每次询问都需要一个left和right,然后分别球的每一次询问的过程中的最大值,很显然,我们需要外层再套一个循环,表示询问每一次,然后再执行打擂台,那么就是这样的:

for (int j=0;j<m;j++)
{
	for (int i=left;i<right;i++) //假定left,right满足下标范围
	{
		//取最大值
		if (numMax<nums[i]) numMax=nums[i];
		else if (numMin>nums[i]) numMin=nums[i];
	}
}

m表示询问的次数,n表示某一区间的范围,则可以得到算法的时间复杂度 O(mn),它的效率是非常低的。

有没有什么办法可以改善在区间中查询最大(小)值的算法呢?


ST算法

ST算法是求解RMQ问题的优秀算法,它适用于静态空间的RMQ查询。就类似于刚才我们所引入的那个问题,就是静态空间的RMQ


ST算法的原理:
我们可以把整个区间分成多个子区间,假设我们通过某种方法之前提前知道了这两个子区间的最值,则很轻松,我们可以立马得知整个区间的最小值。

但是这样做我们必须首先基于以下的事实: 整个大区间必须被两个子区间所覆盖,相当于两个子区间的并集必须就是整个区间,两个子区间的范围可以覆盖,所以我们才能得到上述的结论。

在这里插入图片描述

这便是ST算法的基本思想

1. 我们把整个区间划分为合适的子区间,然后求得子区间的最值。
2. 对于任意一个区间的最值查询,我们都可以转换为求覆盖了它的两个子区间的最值,利用这两个子区间的最值求得这个任意区间的最值。


那么我们明白了,想要求某个区间的最值,转换为两个子区间的最值,然后直接把他们的最值进行比较就得到了需要求得区间的最值。 我们可以利用这个方法求得任意区间的最值,所以我们面临一个问题,我们该如何划分这个数组呢,才能使得任意一个区间我们都可以得到子区间的最值。

划分数组成子数组的方式:利用倍增的思想


倍增

倍增顾名思义: 成倍增加。 其实它与另一个我们所熟知的算法成一个对立的关系:二分查找

二分查找:不断缩小区间,直到得到了符合条件的最小的区间,得到结果。
倍增:不断增大区间,直到某一个条件。。。

二分查找算法在大多数问题中的时间复杂度是O(nlog2^n)
倍增思想在大多数问题中的时间复杂度也是O(nlog2^n)

我们再来思考这样一个问题:倍增难道就是简单的每次区间长度扩大二倍吗? 不是的,难道二分查找就是每次把区间缩小二倍?很显然不是,二分查找的缩小性与 log2^ n有关,我们可以得到每次二分都会缩小 log2^n倍,所以我们的倍增就可以理解为每次增大log2 ^n倍。

倍增:1 -> 2 -> 4 -> 8 -> 16 … -> 2^k ,假如我们所要求的数是16,那么我们从1总共需要倍增4次, k = log2 ^ n n=2 ^ k k=4


我们的区间也可以按照这种方式来划分,每次划分 2 ^ k 个长度的范围,k从0开始:

  • 总共有 k = log2 ^ n 组,其中 n = 10(区间的总长度),所以向下取整组数: k = 3 k从0 开始,闭区间 [0,3] 。
  • 每组划分的子区间长度是 2 ^ k每组可以划分的最多块数: (n - 2^ k +1)
  • 第一组:k = 0 每块长度为2^0 = 1,划分10(n - 2 ^ 0 + 1)块。
  • 第二组:k =1 每块长度为2^1 =2,划分9(n - 2 ^ 1 + 1)块。
  • 第三组:k =2 每块长度为2^2 =4,划分7(n - 2 ^ 2 + 1)块。
  • 第四组:k =3 每块长度为2^3 =8,划分3(n - 2 ^ 3 + 1)块。
    在这里插入图片描述
    这时我们便可以发现,每组每块区间的最值都可以由前一组递推而来:
  • k = 1 :第一块:元素[4,6]的最小值是 4,由上一组(k=0)的前两块[4],[6]的最小值得到。
  • k = 2:第一块:元素[4,6,9,8]的最小值是4,由上一组(k=1)的第一块[ 4,6 ]和第三块[ 9,8 ]得到,而这两小块又可以通过第一步(k=1时)由k=0的对应的位置两小块得到。
  • k = 3:第一块:元素 [4,6,9,8,7,6,3,1]的最小值是1,由上一组(k=2)的第一块[4,6,9,8]和第四块(图中未画出)[7,6,3,1]得到,同样这两个子块,我们之前肯定已经求过了。

这样看,我们似乎得到了一个动态规划的过程:求解当前问题,可以转换为n个子问题,然后由子问题的答案得到当前答案的最优解。


我们创建dp二维数组,规定:

  • s:(start)表示 每组中每一块的起始位置(下标0开始)
  • k:k标识组,进而得到每一组的每一块的区间长度: 2 ^ k
  • dp[s][kl] :表示左端点为 s,区间长度为 2 ^ k的区间的最值。

我们可以推导出这个动态规划的递推公式:

在这里插入图片描述

这个dp公式咋这么复杂啊,还是有 s+ 2^(k-1) 是啥意思啊,我们接下来就来一步一步推导出这个dp公式。


ST递推公式

  1. 首先我们得知,第一组 k=0时,随着起始点s的变化,每块子区间的长度总是0,因此dp[0][0],dp[1][0],dp[2][0],dp[3][0]…dp[n-1][0]分别表示起始点是0,1,2,3…一直到n-1最后一个元素,有10块,每块子区间的长度是0,所以每块子区间的最小值一定是这个位置的元素本身。
    在这里插入图片描述

  2. 第二组:k = 1 时,每块子区间的区间长度是2,因此dp[0][1] 表示起始点0,区间长度是2的子区间的最小值是(由上一组推出,刚才已经说过了,min(4,6)=4);dp[1][1]表示起始点1,区间长度是2的子区间的最小值是(min(6,9)=6),同理可以画出第二组的图像:

在这里插入图片描述

  1. 第三组:k = 2 时,每块子区间的区间长度是4(2 ^ 2),因此dp[0][2] 表示起始点0,区间长度是4的子区间的最小值是(由前一组推出,min([4,6],[9,8])=4);dp[1][2]表示起始点1,区间长度是4的子区间的最小值是(min([6,9],[8,7])=6),同理可以画出第三组的图像:

在这里插入图片描述

  1. 第四组:k = 3 时,每块子区间的区间长度是8(2 ^ 3),因此dp[0][3] 表示起始点0,区间长度是8的子区间的最小值是(由前一组推出,min([4,6,9,8],[7,6,3,1])=1);dp[1][3]表示起始点1,区间长度是8的子区间的最小值是(min([6,9,8,7],[6,3,1,2])=1),同理可以画出第四组的图像:

在这里插入图片描述

k 属于 [0,3],所以遍历结束到结束,

可以看到:

  • 当我们填充每一列的时候,随着 k 的不同,我们的dp公式略有不同,因为k代表了每一块子区间的长度,为 2^k,所以我们由 前一个k组推出当前k的方式也略有不同,综上:我们的dp公式如下所示:

在这里插入图片描述


查询任意区间的最值

对于需要查询的任意一个区间 [L,R],它的起始点是L,终点是R(规定L<=R),这些区间的交集就是 [L,R]。

求任意区间最值的方法:

刚才我们已经划分了每一组每一块子区间,现在我们就可以认为:以L为起点的区间,它的后面包含有长度 1 ,2,4,8… 的子区间,以R为终点,它的前面也包含有 1,2,4,8…的子区间我们可以把需要查询的区间分成任意的两个等长的子区间,这两个子区间的起点和终点分别是L和R,并且两个子区间一定覆盖(交集)需查询区间, 区间最值便可以由这两个小区间的最值得到,时间复杂度是O(1)

看图:

当我们需要查询的区间L=4,R=9的时候:即需要求dp[4][k]的值即可,注意这个k不是图中的k=3,这个k表示的含义是区间长度是 2 ^ k 的区间。根据我们上面分析求任意区间的最值的方法,我们需要把 [L,R]这一区间分成两块长度为 k 的子区间,但是这个 k 如何确定呢?

在这里插入图片描述


如何确定 k ?

  1. 首先我们可以根据 L 和 R求出这块区间的长度:len = R-L+1,得到了长度之后,我们可以知道: 2 ^ k< len <= 2^(k+1) ,即总长度len一定大于划分的两块子区间的长度,并且这两个子区间的长度一定要取得最大值。我们把数据先带入求k,可以得到 k=2k=2就是两个子区间的长度:2^k =4,所以求k的方法:k= log2(len)
  2. 两块子区间的长度是2^k,并且还需要满足两个子区间分别位于 L 和 R上,需要覆盖整个子区间,因此这两个子区间的划分如图所示。
  3. 我们只需要求出这两个子区间[L,L+4] 和 [R -(2^k)+1,R]的最值,就可以得到 [L,R]区间的最值。
  4. 所以求任意区间的最值的公式
    在这里插入图片描述
    在这里插入图片描述
auto getnum = [=](int left, int right)
{
	int k = log2(right - left + 1);
	return max(dp[left][k], dp[right - (1 << k) + 1][k]);
};

代码实现

所以我们的ST求解某一个区间的最值就已经完成了,有了dp这个数组,我们就可以做到在 O(1) 时间里找到 起始点为left,终点为right的 子区间的最值,因为我们每一个子区间的最值早已记录在dp数组里了。

请注意:我们让区间的下标从1开始 [1,n],这样做是为了在做题时方便,当然从0开始也可以,这时我们在init_dp的时候,s就要从0开始,并且s < n - (1 << k) + 1,由于很多OJ题目都是从1开始的,所以最后我们在求getnum的时候,需要L-1,R-1,当然我们可以直接让下标从1开始就可以了。

求区间最小值:min,求最大值直接把min换为max即可。

namespace test25
{
	int n, m;
	constexpr int maxnum = 500005;
	vector<int> vec(maxnum);
	//dp数组:行数表示s,列数表示k,假定k能够表示足够大的数字(2^k)
	vector<vector<int>> dp(maxnum, vector<int>(40));
	void init_dp()
	{
		//第一组:区间长度是1的初始化
		for (int s = 1; s <= n; s++)
		{
			dp[s][0] = vec[s];
		}
		//求出最大能容纳多少组
		int p = log2(n);
		//对于每一组:1,2,3 ... p
		for (int k = 1; k <= p; k++)
		{
			//求出s为 1,2,3...n-1的对应的每个区间的最值
			for (int s = 1; s <= n - (1 << k) + 1; s++)
			{
				dp[s][k] = min(dp[s][k - 1], dp[s + (1 << (k - 1))][k - 1]);
			}
		}
	}
	int getnum(int L, int R)
	{
		int len = R - L + 1;		//需查询区间长度
		int k = log2(len);		//划分成两个子区间,子区间的最大长度
		return min(dp[L][k], dp[R - (1 << k) + 1][k]);
	}
	void test()
	{
		//n表示区间的长度,m表示询问的次数
		cin >> n >> m;	
		
		for (int i = 1; i <= n; i++)
		{
			scanf("%d", &vec[i]);
		}
		init_dp();
		//打印,查询结果正确性
		/*int p = log2(n);
		for (int s = 1; s <= n; s++)
		{
			for (int k = 0; k <= p; k++)
			{
				cout << dp[s][k] << " ";
			}
			cout << endl;
		}*/
		//对于每一次询问
		for (int i = 0; i < m; i++)
		{
			int L, R;
			scanf("%d%d", &L, &R);
			printf("%d\n", getnum(L, R));
		}
	}
}

打印结果如下:
在这里插入图片描述
这与我们上面自己推导出来的dp数组的值完全一样,不信你对比一下。
在这里插入图片描述
在这里插入图片描述

L = 1,R = 5 内的最小值是4
L= 2,R = 6内的最小值是6

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

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

相关文章

Triumph X 的 I LOVE KARACTER——NFT 系列来啦!

I LOVE KARACTER 是一个由韩国角色组成的元宇宙世界&#xff0c;其主要商业模式&#xff08;BM&#xff09;是一个基于角色的元宇宙模型代理&#xff0c;可以在元宇宙宣传中心使用选定的角色作为模型。 为庆祝与 The Sandbox 的合作&#xff0c;Triumph X 发布了 I LOVE KARACT…

Vivado 综合约束实用命令(更新中……)

引言本文记录一些用于 Vivado 综合约束的实用命令&#xff0c;欢迎补充~本文会适当结合一些特定设计进行解释&#xff0c;并结合相关工程进行具体的综合实现分析&#xff0c;不只是理论知识还有实际操作。演示使用的Vivado 版本&#xff1a;2018.3FPGA芯片型号&#xff1a;xc7a…

基于Java+SpringBoot+vue+elementui药品商城采购系统详细设计实现

基于JavaSpringBootvueelementui药品商城采购系统详细设计实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文…

【vue2】常见指令的用法与示例

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;vue指令的声明与使用&#xff08;v-text、v-html、v-on、v-bind、v-for、v-model、v-if、…

C++继承与类的静态成员

什么是继承&#xff1f; 继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能。这样产生的新类&#xff0c;称派生类&#xff08;或子类&#xff09;&#xff0c;被继承的类…

【多标签文本分类】《基于标签语义注意力的多标签文本分类》

阅读摘要&#xff1a;   为了建立标签的语义信息和文档的内容信息之间的联系并加以利用&#xff0c;文章提出了一种基于标签语义注意力的多标签文本分类(LAbel Semantic Attention Multi-label Classification,简称 LASA)方法。 参考文献&#xff1a;   [1] 基于标签语义注…

【nowcoder】笔试强训Day17

目录 一、选择题 二、编程题 2.1杨辉三角的变形 2.2计算某字符出现次数 一、选择题 1.一个查询语句执行后显示的结果为&#xff1a; 1班 80 2班 75 3班 NULL &#xff0c;则最有可能的查询语句是&#xff08;&#xff09; A.SELECT AVG(成绩) FROM 成绩表 WHERE class<…

WindowManager

1 Window、WindowManager 和 WMS Window 是一个抽象类&#xff0c;具体的实现类为 PhoneWindow&#xff0c;它对 View 进行管理。WindowManager 是一个接口类&#xff0c;继承自接口ViewManager&#xff0c;它是用来管理 Window 的&#xff0c;它的实现类为 WindowManagerImpl…

[Leetcode] 二叉树的深度、平衡二叉树

题目链接&#xff1a;二叉树的最大深度 https://leetcode.cn/problems/maximum-depth-of-binary-tree/submissions/二叉树的最小深度 https://leetcode.cn/problems/minimum-depth-of-binary-tree/平衡二叉树 https://leetcode.cn/problems/balanced-binary-tree1.二叉树的最大…

微信小程序实现上下左右滑动触发联动选项卡、绝对值、事件、parse、stringify、Math、atan、abs、findIndex

文章目录序言1、HTML部分2、JavaScript部分&#xff08;上下左右滑动均触发&#xff09;3、JavaScript部分&#xff08;左右滑动触发&#xff09;4、效果演示序言 最近在写原生微信小程序项目的时候遇到了左右滑动内容更新数据&#xff0c;同时改变tabBar的高亮效果。于是就写了…

B. Camp Schedule(KMPnext数组)

Problem - 1137B - Codeforces 在全国范围内广为人知的春季编程训练营即将开始。因此&#xff0c;所有友好的策展人和教师团队开始组成营地的时间表。经过不断的讨论&#xff0c;他们想出了一个时间表&#xff0c;可以表示为一个二进制字符串&#xff0c;其中第i个符号是 "…

前后端的身份认证

1、Web 开发模式 目前主流的 Web 开发模式有两种&#xff0c;分别是&#xff1a; 基于服务端渲染的传统 Web 开发模式基于前后端分离的新型 Web 开发模式 1.1、服务端渲染的 Web 开发模式 服务端渲染的概念&#xff1a;服务器发送给客户端的 HTML 页面&#xff0c;是在服务器…

【Linux】进程间通信(万字详解) —— 下篇

&#x1f387;Linux&#xff1a; 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看见坚持…

我的周刊(第073期)

我的信息周刊&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。&#x1f3af; 项目zlib-searcher[1]zlib 开源搜索方案&#xff08;zli…

基于朴素贝叶斯算法的激光雷达点云分类

前言激光雷达技术是一种采集三维数据的、重建三维模型的手段&#xff0c;运用在各个行业&#xff0c;随着激光雷达技术的发展与广泛运用本文采用监督分类中的朴素贝叶斯算法进行地基于激光雷达的地物分类。首先根据点云的几何位置建立邻域范围&#xff0c;借助邻域点的集合计算…

分享98个PHP源码,总有一款适合您

PHP源码 分享98个PHP源码&#xff0c;总有一款适合您 PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1ZNcdj0bLY51UXNoXq8tgFg?pwdwn4b 提取码&#xff1a;wn4b 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0…

ORB-SLAM2 --- LoopClosing::ComputeSim3 函数

目录 1.什么是sim3&#xff0c;为什么要做sim3 2.函数流程 3.code 4.函数解析 4.1 准备工作 4.2 遍历闭环候选帧集&#xff0c;初步筛选出与当前关键帧的匹配特征点数大于20的候选帧集合&#xff0c;并为每一个候选帧构造一个Sim3Solver 4.3 对每一个候选帧用Sim3Sol…

实例分析Linux内存泄漏检测方法

一、mtrace分析内存泄露 mtrace&#xff08;memory trace&#xff09;&#xff0c;是 GNU Glibc 自带的内存问题检测工具&#xff0c;它可以用来协助定位内存泄露问题。它的实现源码在glibc源码的malloc目录下&#xff0c;其基本设计原理为设计一个函数 void mtrace ()&#x…

解决Tinkphp的success跳转“使用路由别名后模块和路由器访问不了”问题

遇到的问题&#xff1a;我的thinkphp5网站添加了以下路由别名&#xff1a;Route::alias([ index>index/index, ]);使用http://域名/Index/user/password.html访问正常但使用http://域名/index/user/password.html就访问失败使用$this->success(修改密码成功);进行提示跳转…

Java 链表与LinkedList

链表的组合形式 ①有头结点、无头结点 ②单向链表、双向链表 ③循环链表、非循环链表 根据自由组合&#xff0c;可以得到8种不同形式的链表&#xff0c;那么在刷题种常碰到的是不带有头结点的单向非循环链表和不带头结点的双向非循环链表。 模拟实现不带头结点的单向非循环链表…