基于ST表解决RMQ(区间最值查询)问题
第二篇题解 2023/1/27 21:00
pre: ST表的本质其实是动态规划,但因为我到现在也没弄懂动态规划,所以我更愿意称ST表是基于倍增思想(二进制下的倍增)的。
其实使用线段树可以实现ST表的大部分功能,但线段树板子太长,细节太多,太容易出错,再加上我比较懒,所以就更喜欢ST表一些。同时,线段树的查询效率是O(logn),而ST表却是O(1),这一下就甩开了差距,所以在区间最值的问题中,ST表大部分情况下是作为首选的。
ST表就是一张二维表,用二维数组来存储值。在数组维度的含义表示的层面,利用了倍增思想。
利用ST表求RMQ问题之所以效率高是因为我们提前在预处理时通过二的某次方的的区间跳跃,求得了了该区间的最大值,其本质其实就是递推,只不过是倍增的递推
关于倍增,大家可移步【朝夕的ACM笔记】算法基础-倍增 - 知乎 (zhihu.com)
以下以ST表求最大值为例
预处理
首先ST表的预先处理为以下所示
其中,st[i][j] 表示以i为起点,长度为2的j次方的区间的最值,其实就是区间[i,i+2^j-1]的闭区间的最值
这里的递推是自底向上的,先处理完长度为2^0的区间,也就是每个长度为1的区间[i,i]
接着利用递推公式处理长度更大的区间的最值,依次处理之后就可以得到每个二的某次方区间的最值了
详细预处理代码如下
int st_max[N][log(N)];
void initialize()
{
for(int i=1;i<=n;i++)
{
st_max[i][0]=a[i];//表示区间[i,i+2^0-1]的最值为自己的值(i+j^0-1其实就是i)
}
for(int j=1;(1<<j)<=n;j++)//1<<j的意思是2的j次方(1是2的0次方,1向右移j位)
for(int i=1;i+(1<<j)-1<=n;i++)
{//max函数括号里的前一半是区间[i,i+2^j-1]的前半个区间,后一半是后半个区间
st_max[i][j]=max(st_max[i][j-1],st_max[i+(1<<(j-1))][j-1]);
}
}
因此,我们ST表初始化的核心递推公式就是
st[i][j] = min(st[i][j - 1],st[i + 2^(j - 1))][j - 1])
查询操作
接下来就是区间
首先我们要知道有这么一个式子——2^log(length)>length/2
大家随便举几个数的例子就可以理解了
但是为什么要这样做呢?
因为,前文中我们介绍的预处理只是将区间长度为2的某次方的最值求了出来,但是给定的查询区间的长度不一定是2的某次方,这时候我们就要将查询的区间构造成两个2的某次方长度的区间
通过上边的不等式我们可以得出结论——两个区间不一定等分,但两个区间加起来一定完全覆盖了当前的那个大区间
那么这个划分两个区间的标尺是什么呢?
就是上文中提到的2^log(length)>length/2对应的一个长度lg——log(length)。
假设大区间为[l,r]
这样左边的区间为[l,l+2lg-1],右区间为[r-2lg+1,r]。通过比较这两个区间的最值的到最后大区间的最值
这样我们就实现了对区间查询的操作
具体的本题AC代码如下题目传送门
ps: 1<<lg,就是2的lg次方,因为1本身就是2的0次方,对1进行位运算其实就是在二进制的规则下对2进行位运算
#include <iostream>
#include <cstdio>
#include<cmath>
#include <algorithm>
using namespace std;
const int N = 5*1e4+10;
int a[N],st_max[N][30],st_min[N][30],n,q;
void initialize()
{
for(int i=1;i<=n;i++)
{
st_max[i][0]=a[i];//表示区间[i,i+2^0-1]的最值为自己的值(i+j^0-1其实就是i)
st_min[i][0]=a[i];
}
for(int j=1;(1<<j)<=n;j++)//1<<j的意思是2的j次方(1是2的0次方,1向右移j位)
for(int i=1;i+(1<<j)-1<=n;i++)
{//max/min函数括号里的前一半是区间[i,i+2^j-1]的前半个区间,后一半是后半个区间
st_max[i][j]=max(st_max[i][j-1],st_max[i+(1<<(j-1))][j-1]);
st_min[i][j]=min(st_min[i][j-1],st_min[i+(1<<(j-1))][j-1]);
}
}
int search_max(int l,int r)
{
int lg=log(r-l+1)/log(2);//把区间从中间附近劈开,不一定是正中间哦
return max(st_max[l][lg],st_max[r-(1<<lg)+1][lg]);
}
int search_min(int l,int r)
{
int lg=log(r-l+1)/log(2);
return min(st_min[l][lg],st_min[r-(1<<lg)+1][lg]);
}
int main()
{
cin>>n>>q;
for(int i=1;i<=n;i++)scanf("%d",a+i);
initialize();
while(q--)
{
int l,r;
scanf("%d %d",&l,&r);
printf("%d\n",search_max(l,r)-search_min(l,r));
}
return 0;
}//45行————186msAC——scanf在大规模数据下优势比较明显
如有不周之处,还望大家不吝指正。
如果感到有帮助的话,希望给我留个赞赞和收藏哦。