ST(Sparse Table,稀疏表)算法是求解RMQ问题的经典在线算法,以O(nlogn)时间预处理,然后在O(1)时间内回答每个查询。
ST算法本质上是动态规划算法,定义了一个二维辅助数组st[n][n],st[i][j]表示原数组a中从下标i开始,长度为2^j的子数组中的最值(以最小值为例)。
要求解st[i][j]时,即求下标i开始,长度为2^j的子数组的最小值时,可以把这段子数组再划分成两半,每半的长度为2^(j-1),于是前一半的最小值为st[i][j-1],后一半的最小值为st[i+2^(j-1)][j-1],于是动态规划的转移方程为:
st[i][j] = min(st[i][j-1], st[i+2^(j-1)][j-1])
长度为2^j的情况只和长度为2^(j-1)的情况有关,只需要初始化长度为2^0=1的情况即可。而长度为1时的最小值是显然的(为其本身)。现在问题是,st数组可以怎样加速我们的查询呢?
这也是算法的巧妙之处,假设求下标在u到v之间的最小值。先求u和v之间的长度len=v-u+1,然后求k=log2(len),则u到v之间的子数组可以分为两部分:
以u开始,长度为2^k的一段
以v结束,长度为2^k的一段(可以计算得到起始位置为v-2^k+1)
ST算法本质上是动态规划算法,定义了一个二维辅助数组st[n][n],st[i][j]表示原数组a中从下标i开始,长度为2^j的子数组中的最值(以最小值为例)。
要求解st[i][j]时,即求下标i开始,长度为2^j的子数组的最小值时,可以把这段子数组再划分成两半,每半的长度为2^(j-1),于是前一半的最小值为st[i][j-1],后一半的最小值为st[i+2^(j-1)][j-1],于是动态规划的转移方程为:
st[i][j] = min(st[i][j-1], st[i+2^(j-1)][j-1])
长度为2^j的情况只和长度为2^(j-1)的情况有关,只需要初始化长度为2^0=1的情况即可。而长度为1时的最小值是显然的(为其本身)。现在问题是,st数组可以怎样加速我们的查询呢?
这也是算法的巧妙之处,假设求下标在u到v之间的最小值。先求u和v之间的长度len=v-u+1,然后求k=log2(len),则u到v之间的子数组可以分为两部分:
以u开始,长度为2^k的一段
以v结束,长度为2^k的一段(可以计算得到起始位置为v-2^k+1)
注意,一般情况下这两段是重叠的,但是这两段的最小值中较小的一个仍然是u到v的最小值。于是
RMQ(u,v) = min(st[u][k], st[v-2^k+1][k])
C++代码实现如下:
#include<iostream>
#include<math.h>
using namespace std;
int ST[10005][30]; <span style="white-space:pre"> </span> //用于动态规划的st数组
int n; <span style="white-space:pre"> </span> //数组中元素的个数
int A[10005];
/*
ST[i][j]数组表示以节点i为起点,长度为2^j个数组段中,最小的数
*/
void initST(){
for(int i=0;i<n;i++) <span style="white-space:pre"> </span> //长度为2^0(也就是长度为1)的最小值就是本身
ST[i][0]=A[i];
}
/*
ST[i][j]=min{ST[i][j-1],ST[i+2^(j-1)+1][j-1]}
这是一种很聪明的递推,只要是和2的次幂有关的表达式都能用这个递推,
就是将2^j分成两段,每段的长度都是2^(j-1),这样由于我们很容易的求得了
j为0的情况,然后就可以很容易递推出后面的情况
*/
int getTwo(int i){
int res=1;
while(i>0){
res*=2;
i--;
}
return res;
}
void calST(){
int logs=(int)(log(n)/log(2)); <span style="white-space:pre"> </span>//求出最多向上递归的次数
for(int j=1;j<=logs;j++){ <span style="white-space:pre"> </span> //因为j=0的情况在initST中已经算了
for(int i=0;i<n;i++){
int tmp=getTwo(j-1);
if(i+tmp+1<n){ <span style="white-space:pre"> </span>//即两段都存在
int tmp=getTwo(j-1);
ST[i][j]=ST[i][j-1]>ST[i+tmp][j-1]?ST[i+tmp][j-1]:ST[i][j-1];//二者当中取相对小的
}
else
ST[i][j]=ST[i][j-1]; //只剩下前半段,则只用前半段
}
}
}
/*
查询的时候我们先将求出这个区间的log2值logs,然后将区间分为前后两段,
前一段是以u开头,长度为2^logs,
后一段是以v结尾,长度为2^logs
虽然这样会有重复,但依然不会影响区间最小值,
因为这些重复值也在区间呢
*/
int RMQ(int u,int v){
int logs=(int)(log(v-u+1)/log(2)); <span style="white-space:pre"> </span>//区间长度最大2的多少次幂
int res=ST[u][logs]>ST[v-getTwo(logs)+1][logs]?ST[v-getTwo(logs)+1][logs]:ST[u][logs];
return res;
}
void init(){
initST();
calST();
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>A[i];
}
init();
int m;
int u,v;
cin>>m;
for(i=0;i<m;i++){
cin>>u>>v;
cout<<RMQ(u,v)<<endl;
}
return 0;
}