动态规划算法之ST算法

运用场景

         RMQ (Range Minimum/Maximum Query):对于长度为n的数组A,回答若干询问RMQ(A,i,j)(i,j<=n-1),返回数组A中下标在i,j范围内的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。最简单的方法,就是遍历数组直接搜索,但是这种方式时间复杂度是O(n)。对于数组长度较大,性能要求高的场景不适用。下面我仅仅以最大值来举例。

         通俗点就是说:给你一个序列:9 3 1 7 5 6 0 8
然后给你一个区间,假如是[1,4],也即是要求(9,3,1,7)里面的最大数。注意:题目中的区间范围是从1开始,而不是从0开始的。

理论基础

         任何一个正整数都可以拆分为任意多个2的次幂的和。2的次幂有:1,2,4,8,16,32…例如,17= 2^4 + 2^0,20= 2^4 + 2^2。如果你还有点半信半疑,那么在你学习将10进制数据转化为2进制数据的时候,老师是不是要你将10进制数写成多个1,2,4,8…的组合,并且要首先找到一个最接近这个10进制数据的数。例如:20=16+4。所以,任意一个正整数都可以转化为任意多个2的次幂数据的和。

         那么这个理论和我们说的ST算法有什么关系吗?其实这个也是从LCA问题迁移过来的(因为我首先是遇到了LCA问题,然后找了倍增算法,发现倍增算法里面涉及到了ST算法的思想,所以就先来了解ST算法的)。回到主题,我们想要求出区间 [1,4],也即是要求(9,3,1,7)里面的最大值,那么是不是可以转化为这样的情况:我假如知道了[1,2]和[3,4]里面的最大值,那么他们两个的最大值是不是就是[1,4]的最大值了(有点分治的思想)。那么知道[1,2]的最大值,是不是就得提前知道[1,1]和[2,2]分别的最大值,然后求出他们的最大值。如果采用动态规划的话,我们使用一个数组dp[m] [n] ,dp[i][j]=d 表示的就是从下标 i 开始的 j 个元素的最大值就是d。那么 j 的范围是不是要从1到N(因为你的序列一共有N个元素)。重点来了: 换句话说,1<=j<=N。传统的做法就是dp[][] 开一个maxn*maxn的数组,dp[1][2]就是代表下标从1开始的2个元素之间的最大值。但是这样子N有多大,你的数组就得开N * N的大小。但是有了前面的理论,可以使用2的次幂的数据来代表1~N。也就是说,dp[i][j]就代表下标从 i 开始,一共 2^j 个元素的长度的最大值。从表达式来来说就是:dp[i][j]=D,表示区间[i,i+2^j-1]中最大数是D。这个也是为什么大家都说ST算法的预处理阶段的时间复杂度是O(nlogn)。如果不采用倍增的方式的话,时间复杂度就是O(N ^ 2)

         如果还是有点不清楚的话,这里是我在看LCA问题时,发现作者隐含的提到了这个问题。看一下作者说到倍增法的退化版,就知道为什么要使用倍增法来建立动态规划了,以及前面的理论的用处了

预处理和查询

        我在最开始学习这个ST算法的时候,就是觉得它有点像动态规划(毕竟这个东西理解的还是不是特别透彻),但是看了网络上很多博客,都没有几个博客提到了这个是动态规划。但是,还是找到了一篇,指明了ST算法其实就是一种动态规划的算法。通过我前面的描述,应该也可以理解这个是一个动态规划的问题。具体这里的细节大家可以先去看看我后面的参考博客,然后不懂的地方就来我这里看,这样子就能理解的清楚一点。

预处理

        设A[i]是要求区间最值的数列,F[i, j]表示从第i个数起连续2^j个数中的最大值。(DP的状态)

例如:

A数列为:3 2 4 5 6 8 1 2 9 7

F[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。同理 F[1,1] = max(3,2) = 3, F[1,2]=max(3,2,4,5) = 5,F[1,3] = max(3,2,4,5,6,8,1,2) = 8;

并且我们可以容易的看出F[i,0]就等于A[i]。(DP的初始值)
我们把F[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从 i 到i + 2 ^ (j - 1) - 1为一段,i + 2 ^ (j - 1)到i + 2 ^ j - 1为一段(长度都为2 ^ (j - 1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。F[i,j]就是这两段各自最大值中的最大值。于是我们得到了状态转移方程F[i, j]=max(F[i,j-1], F[i + 2^(j-1),j-1])。

这我提一下这句话

我们把F[i,j]平均分成两段(因为f[i,j]一定是偶数个数字)

        我一开始就不是特别能够理解这句话,但是其实博主是想要表达这样一句话:因为F[i,j]代表的区间长度是[i,i+2^j-1],那么长度就是i+2 ^j-1-i+1= 2 ^j,那么这个长度肯定就是一个偶数,因为是2的次幂,并且 i 是从1开始的。

        其实这里还隐含了这样的一个事实。就是F[i][j]仅仅只能表达是在一个偶数长度区间内的最大值。也就是说,如果题目给的是一个奇数长度的区间,是不能直接从F[ ][ ]数组里面取出来的。具体如何操作,就是下面查询要做的事情了。

查询

        因为题目给出的区间长度可能不是一个偶数,那么我们是无法通过F[][]数组直接获得的。那么怎么处理呢?处理方法如下:还是使用上面的A数列,假如我们要寻找的区间是[3,7],那么实践上数据就是(4 5 6 8 1)一共5个数据。所以直接使用F[][]数组获取不到。所以,我们将这个区间拆分成(4,5,6,8)和(5,6,8,1)两个偶数长度的区间。不用担心有重复数据的问题,因为我们是要求出最大值,所以有重复值也是可以的。(4,5,6,8)中最大值是8;(5,6,8,1)的最大值也是8;所以(4 5 6 8 1)的最大值就是8.。这样子就可以直接从F数组里面分别获取,然后取max就可以了。那么问题就转化为到底选一个怎么样的区间长度呢?也就是说:如果当题目给出的区间长度是一个奇数,那么应该怎么分成两个偶数区间呢?很多博客就是在这里就是直接告诉大家结果,但是就是没有细讲为什么这样子取值。下面我来讲讲自己的理解。

        因为我们要找到区间[3,7]的最大值,如果要使用f[][]数组来得到这个结果的话,那么就要覆盖[3,7]长度为5的区间。因为f[][]数组都是代表一些偶数长度的区间最大值,所以直接使用f[][]数组的话,势必就只能用一个大于5的区间来求,例如长度为6的区间。但是这样子可能会出现错误,因为可能就多的一个数据因此没有得到一个正确的最大值的结果。那么还有一个方法就是使用具有重叠区间的长度(说实话,要我直接去想,我觉得我可能想不到)。为了更好的讲述,我现在使用数学表达式来解释。

        题目要求区间[i,j]的最大值。区间[i,j]的长度就是j-i+1;如果f[i][k]要代表这个区间长度的话,2^k=j-i+1,所以k=log2(j-i+1);这个就是其他博客上这个式子的由来。但是从数学表达式的角度来看,f[i][k]已经可以代表这个区间了。但是在编程的时候会有强制取整的,所以k就会变小(因为你定义的k是一个int型,但是log2(j-i+1)可能得到的就是一个double。所以会变小)。所以我们首先采用F[i][k]代表的区间就是[i,i+2 ^k-1],然后需要f[][]从区间j往前推2 ^k个长度的区间,所以还一个就是F[j - 2 ^k+1][k]。这个式子换成代码里面的写法就是:F[j-(1<<k)+1][k]。有的同学可能会担心,第一个从下标 i 往后推2 ^k长度,还一个从下标 j 往前推2 ^k长度,会不会还有一部分没有覆盖到完整的[i,j]。这里我觉得应该是严格的证明。因为要证明严格覆盖,所以仅仅证明后一个区间的起点在前一个区间的终点的前面就好了。
在这里插入图片描述

完整代码

#include<iostream>
#include<cmath>
#include<algorithm>

using namespace std;
const int maxn = 10010;

//f[i][j]表示从i位起来,2^j个数中的最大数
//也就是在[i,i+2^j-1]区间中的最大数
//maxn是数据最大的长度,也就是题目给出的序列中的最大长度
//20是因为每一次倍增2^j,如果j的最大值为20的话,基本上题目长度不会超过它
//也就是要保证maxn<=2^20
int f[maxn][20];

//存放数据的数组
int arr[maxn];

int N;//序列中长度
int M;//查询次数

//ST算法预处理阶段
void st_pre()
{
	//长度为1的都初始为自己
	//因为实际的序列长度为N,而且这里的i从1开始计算的
	//所以实际的输入也要从arr[i]开始输入
	//如果不从1开始计算的话,就会导致后面在拆分成两个子序列的时候,出现是负数的清况。
	//还有一个原因就是:题目的给出的区间范围是从1开始计算的
	for (int i = 1; i <=N; i++)
	{
		f[i][0] = arr[i];
	}

	//正式预处理
	//因为接下来f[j][i]代表[j,j+2^(i-1)]区间中的最大数
	//所以说,2^(i-1)要 <= log2(N)。因为要保证j+2^(i-1)<=N。
	//可以取到 =N 的下标的原因是因为下标从1开始计算的
	int imax = log2(N);
	for (int i = 1; i <= imax; i++)
	{
		/*
		//1.
		//注意这里是j+(1<<i)-1<=N,举个例子来看
		//当i=1的时候,就是在查找从arr[1,N]数组中,每两个元素之间的最大
		//值。也就是从index开始,长度为2的区间的最大值。
		//假设N=5,那么我要计算arr[4,5]区间的最大值,使用f[4][1]来表示
		//但是当j=4的时候,j+(1<<i)=4+(1<<1)=6了,无法计算f[4][1]、
		//所以,这就是为什么一定要-1的原因
		*/
		/*
		//2.
		//这里注意<<左移运算符的优先级是没有加法/减法(+ -)运算符
		//优先级高,所以,要注意()的使用。j + (1 << i) - 1 <= N
		//j+(1<<i-1)反而是利用的这个优先级的关系,先执行i-1,后执行i<<
		*/
		for (int j = 1; j + (1 << i) - 1 <= N; j++)
		{
			f[j][i] = max(f[j][i - 1], f[j + (1 << i - 1)][i - 1]);
		}
	}
}

//ST算法在线查询arr[L,R]中最大值
int st_query(int L, int R)
{
	int k = log2(R - L + 1);//区间长度
	return max(f[L][k], f[R - (1 << k) + 1][k]);
}

int main()
{
	cin >> N >> M;
	//注意:数组有效元素从index=1开始
	for (int i = 1; i <= N; i++)
	{
		cin >> arr[i];
	}
	st_pre();
	int L, R;
	for (int i = 0; i < M; i++)
	{
		cin >> L >> R;
		cout << st_query(L, R) << endl;
	}
	return 0;
}

参考博客:

RMQ (Range Minimum/Maximum Query)算法

ST算法—介绍

理解RMQ问题和ST算法的原理

P3865 【模板】ST表 题解

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值