摘要
ST表是一个用来处理区间最值查询(Range Maximum/Minimum Query)的离线算法。该算法分为离线预处理O(n log n)和查询O(1)两个部分,其用到了倍增的思想。
某个区间查询问题是否适用ST表,关键在于其进行的操作是否允许区间重叠,例如max(a,b,c) = max{max(a,b),max(b,c)}就可以用ST表维护,而区间和问题则不能维护。
问题描述
给定一个长度为n的序列,有m次询问,每次给定区间[L , R],求区间内最大值。
算法思路
定义st表
我们设 st[i][j] 为从 i 开始的 2^j 个数中的最大值。假设这n个数存放的序列a中,根据定义 st[i][j] = max{a[k] | i <= k <= i+2^j - 1}。
查询操作
假设我们已经更新完st表(下文会讲如何在O(n log n)时间内更新),那么我们应该如何利用上述定义的st表查询[L , R]内的最大值呢?
根据我们上述st表的定义,我们发现,我们能在st表中查询的区间(假设为[a,b])都只能从某个位置a开始向后
2
k
−
1
2^k-1
2k−1个区间(k是自然数),所以如果a-b 并不恰好等于
2
k
2^k
2k,那么多出来的部分(即
[
a
+
2
k
,
b
]
[a+2^k , b]
[a+2k,b])我们并不能直接求出来,只能用类似的方法求出
[
b
−
2
k
+
1
,
b
]
[b-2^k+1,b]
[b−2k+1,b]的最大值,那么最终[a,b]的最大值不就是以上两段区间的max吗?当然这里的k要取使得
a
+
2
k
<
b
a+2^k < b
a+2k<b成立的最大值,即
l
o
g
2
b
−
a
+
1
log_2^{b-a+1}
log2b−a+1。
举个例子,求[2 , 10]的最大值可以拆分为求[2 , 9]的最大值和[3 , 10]的最大值。
于是查询操作(查询[L , R] )就顺利成章的是max(st[L][k] , st[R-(1<<k) + 1][k])。
预处理
我们在上述证明了查询操作确实是O(1)的,所以如果我们真的能按照st表定义的那样去更新它,那么该算法就真正的完成了。更新st表用到了动态规划的思想。
更新中仍然用到了倍增的思想,使得更新操作从O(n^2)优化到了O(n log n),初始状态st[x][0] = a[x],st[x][j] = max(st[x][j-1] , st[x + (1<<(j-1)) ][j-1])。其思想和上述重叠查询类似,不再赘述。但是要注意更新顺序,因为其中 j (第二维)才是阶段,而第一维 x 是状态,所以对于 j 的循环要放在最外层。
代码示例
#include<cstdio>
#include<cmath>
#include<iostream>
using namespace std;
int n,m;
const int N = 1e5+10;
int a[N],st[N][25],Log[N];//2^20就过一百万了,完全够用
//初始化st表
void init(){
Log[1] = 0;//预处理log函数
for(int i = 2;i <= n+1;i++) Log[i] = Log[i/2]+1;
for(int i = 1;i <= n;i++) st[i][0] = a[i];
for(int j = 1; (1<<j) <= n;j++){ //涉及到位运算多加括号!
for(int i = 1;i + (1<<(j-1)) <= n;i++){
st[i][j] = max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
}
int ask(int l,int r){
int k = Log[r-l+1];
int mx = max(st[l][k],st[r-(1<<k)+1][k]);
//printf("%d %d\n",k,mx);
return mx;
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d",a+i);
init();
for(int i = 1,l,r;i <= m;i++){
scanf("%d%d",&l,&r);
printf("%d\n",ask(l,r));
}
}