RMQ问题:
RMQ即Range Minimum/Maximum Query,意为在一个数组中,给定一个区间,查询该区间内的最小值/最大值。这个问题大概有三种算法:
一是暴力解法,即遍历这个区间,这种算法处理不了查询次数非常大的问题。
二是线段树,这种方法可以处理存在区间更新的问题,详见:线段树。
第三种就是ST(Sparse Table)算法。ST算法是一种在线算法,在线表示用户每输入一个查询便马上处理一个查询。该算法一般用较长的时间做预处理,待信息充足以后便可以用较少的时间回答每个查询。
ST算法的预处理:
ST算法的预处理实质是动态规划,以最小值为例,假设数组a[5]为:5,2,7,9,4
定义DP的数组为mn。mn[i][j]表示从下标 i 开始的长度为2^j的区间中的最小值。例如mn[1][1]表示从下标1开始的长度为2的区间,即a[1],a[2]中的最小值2。明白这个概念后我们开始推导递推式:
首先找初始条件,我们发现,mn[i][0]表示从i开始的长度为1的区间中的最小值,即这个数本身,那么DP的初始条件就是:mn[i][0]=a[i]。然后求mn[i][j]时我们发现,将长度为2^j的这个区间分为两半(2^j肯定是偶数),左右子区间分别有他们自己的最小值,而这两个最小值中的较小值就是整段区间的最小值。由此可以写出递推式:
mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1])
解释一下这个式子:mn[i][j-1]表示从i开始的长度为2^(j-1)的区间中的最小值,2^(j-1)正好是这段区间的前一半;而1<<j-1其实就是整个区间长度的一半(位运算符优先级低于+/-),所以i+(1<<j-1)就正好是区间后一半的起点,则mn[i+(1<<j-1)][]j-1]就表示区间后一半中的最小值。
总结一下DP的过程:先求出所有长度为1的区间的最小值,然后根据这些最小值,求出所有长度为2的区间的最小值,再根据这些最小值,求出所有长度为4的区间的最小值……
下面根据DP的过程来总结出预处理的伪代码:
void init(int n,int arr[])
{
for(int i=1;i<=n;i++)
DP[i][0]=arr[i];
for(int j=1;j<20;j++)
{
for(int i=1;i+(1<<j)-1<=n;i++)
{
DP[i][j]=min(DP[i][j-1],DP[i+(1<<j-1)][j-1]);
}
}
}
初始条件就不解释了,下面的二重循环比较有意思。
1、为啥j的循环要写在外面?因为根据我们上面总结的DP过程:先求出所有长度为1的,再求长度为2的,再求长度为4的,即我们首先要遍历区间长度,而j就是控制区间长度的变量,所以要将j的循环写在外面。
2、i的循环条件是怎么设的?从数组mn的角度讲,mn[i][j]表示从下标i开始的长度为2^j的区间内的最小值。如果i后面的元素个数不足2^j,那么就无法求出mn[i][j],循环终止条件由此可得,也可以写成:i+(1<<j) <= n+1。
3、j的循环条件怎么设?根据mn数组的概念,j表示长度为2^j的区间内的最小值,所以j应满足:2^j <= n,而2的20次方大约为100多万,所以j的取值其实很小,一般取20~25即可。
ST算法的查询过程:
首先给出结论:设要查询的区间为(l,r),设k=log2(r-l+1)。那么区间内的最小值为:
min( mn[l][k] , mn[r-(1<<k)+1][k] )
解释一下这个式子:mn[l][k]表示从下标为l开始长度为2^k的区间中的最小值,即区间[l,l+2^k-1]。mn[r-(1<<k)+1][k]表示从下标为r-(1<<k)+1,开始长度为2^k的区间中的最小值,即区间[r-(1<<<k)+1,r],所以现在我们只要证明l+2^k-1 >= r-2^k+1就可以保证这两个区间覆盖到了整个的[l,r]。
证明过程如下:
若l+2^k-1 >= r-2^k+1,则移项可得:2^k+2^k >= r-l+2,
即2^(k+1) >= r-l+2,而k=log2(r-l+1),将k代入,
所以上式等于:2*(r-l+1) >= r-l+2,可以解得r-l >= 0,所以r>=l为结论成立的条件,而r,l分别表示区间的终点和起点,一定满足r>=l,得证。
c++代码:
#include<iostream>
#include<cmath>
using namespace std;
const int N=100005;
int a[N];
int mn[N][25];
int n,q,l,r;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
//DP的初始条件
mn[i][0]=a[i];
}
for(int j=1;j<20;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1]);
while(cin>>l>>r)
{
int k=(int)(log((double)(r-l+1))/log(2.0));
cout<<min(mn[l][k],mn[r-(1<<k)+1][k])<<endl;
}
return 0;
}