问题介绍:
RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列a,回答若干询问RMQ(A,i,j)(i, j<=n),返回数列a中下标在i,j之间的最小/大值。
如果只有一次询问,那样只有一遍for就可以搞定,但是如果有许多次询问就无法在很快的时间处理出来。这里最简单的做法是采用暴力搜索,依次搜索给定区间中的元素,找出最值,当查询次数较多时,此方法效率较低。虽然可以采用记忆化搜索,但是当区间范围较大时,需要开辟一个很大的数组,浪费空间。因此常规方法在性能要求高的场景不适用。除此以外,还可以用线段树解决。
这里介绍了一种比较高效的在线算法(ST算法)解决这个问题。
问题解决:ST(Sparse Table)算法
ST(Sparse Table)是一个非常有名的在线处理RMQ问题的算法,该算法一般用较长的时间做预处理,待信息充足以后便可以用较少的时间回答每个查询,它可以在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。ST算法只适用于静态区间求最值,如果是动态的区间,就需要用线段树了。其算法本质为动态规划,我们用dp[i][j] 表示以 i 为起点,连续 2^j 个数中的最大值(最小值),例如 f [ 2 ][ 2 ] 就表示第 2 个数到第 5 个数的最大值(最小值)。
(1)预处理:dp[i][0]表示从下标i开始,连续2^0=1个数中的最大值(最小值),也就是a[i]。对于每一个 dp数组表示的序列,我们都把它拆成两部分,那么转移方程就是:dp [ i ][ j ] = max { dp [ i ][ j - 1 ] , dp [ i + 2^( j - 1 ) ][ j - 1 ] } 这样可以使常规动态规划预处理(区间dp,用dp [ i ][ j ] 表示以i为起点,j为终点的序列中最大值(最小值))的O(n^2)时间复杂度降为O(nlogn)。
注:i + 2^( j - 1 )可以写成位运算的形式i+(1<<(j-1)) 注意括号,这里位运算的默认优先级最低
(2)查询:这里涉及到区间覆盖问题,假如我们需要查询的区间为(i,j),那么我们需要找到覆盖这个闭区间(左边界取i,右边界取j)的最小幂(可以重复,比如查询5,6,7,8,9,我们可以查询5678和6789)。这个区间的长度为j - i + 1,所以我们可以取k=⌊log2( j - i + 1)⌋,则有:RMQ(A, i, j)=max{dp[i][k], dp[ j - 2 ^ k + 1][k]}。 查询操作可以在O(1)时间复杂度内完成。
问题举例
nyoj 119-士兵杀敌(三)就是一道典型RMQ问题。
代码:
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int T = 100005;
//dp[i][j]=max(dp[i][j-1],max[i+2^(j-1)][j-1])
int dpmax[T][21], dpmin[T][21];
int main()
{
//freopen("debug.txt", "r", stdin);
int n, q,beg,end,i,j,tmax,tmin,dis;
scanf("%d%d", &n, &q);
for (i = 1; i <= n; i++)
{
scanf("%d", &dpmax[i][0]);
dpmin[i][0] = dpmax[i][0];
}
//初始化
//for(j=1;(1<<j)<=n;j++)//O(nlogn)
for (j = 1; j<20; j++)
for (i = 1; i + (1 << j) - 1 <= n; i++)
{
dpmax[i][j] = max(dpmax[i][j - 1], dpmax[i + (1 << (j - 1))][j - 1]);
dpmin[i][j] = min(dpmin[i][j - 1], dpmin[i + (1 << (j - 1))][j - 1]);
}
while (q--)
{
scanf("%d%d", &beg, &end);
dis = floor(log2(end - beg+1));
tmax = max(dpmax[beg][dis], dpmax[end - (1 << dis)+1][dis]);
tmin= min(dpmin[beg][dis], dpmin[end -( 1 << dis) + 1][dis]);
printf("%d\n", tmax - tmin);
}
return 0;
}