[笔记]ST表
原题链
算法用途
ST表主要用于解决RMQ问题(区间最值问题),可以做到(O(nlogn))预处理,(O(1))询问
算法描述
ST表利用的是倍增的思想,以求区间最大值为例,我们用(Max[i][j]表示)从i位置开始的(2^j)个数中的最大值,例如(Max[i][1])表示的是(i)位置到第(2^1 - 1)个数,也就是第(i)个数的下一个数,这两个数的最大值.在转移的时候为我们考虑一个(max)操作的性质:(max(a,b,c) = max(max(a,b),max(b,c)))从这个性质我们发现,我们可以由两个较小的,用重叠的区间来推出一个大区间,因此我们可以少维护一些区间.
知道上面的性质后,转移的式子就很好理解了.首先初始化的时候(f[i][0] = a[i])这表示从第(i)个数往后(2^0 - 1 = 0)个数也就是(i)这个数本身的值,所以直接赋值就好.接着对于包含几个数的区间的转移方程是(f[i][j]=max(f[i][j-1],f[i + 2^{j-1}][j-1]))这个式子是由上面的性质推出来的,首先我们假设(i = 1)这样方便计算与描述,那么根据上面的性质,我们可以将从第(1)个元素到第(2^j - 1)个元素的区间划分成从第(1)个元素到第(2^{j-1}个)元素的区间以及从第(2^{j-1} + 1)个元素到第(j)个元素这两个区间分更新最大值,这样就可以从小的区间推出大的区间,这也是符合上面讲的性质的.
再来说查询,查询的时候会遇到很多问题,如果我们只从左端点去找,那么如果是要查询((1,7))的最小值怎么办呢?如果从(1)往后走(2^2),就只能查询((1,3)),但如果往后走(2^3),又查询的是((1,8)),所以并不好做.为了解决这个问题,我们可以直接从查询区间的两端一起找,现将区间的长度写成(2^k),接着从两端(l,r)分别找,找的区间重叠了也没有关系,用代码写这一段就是:
int ask(int x,int y){//查询
int k = log2(y - x + 1 );
return max(maxx[x][k],maxx[y - (1 << k) + 1][k]);
}
但有一点要注意,在从右端点查询的时候为什么要(+1)呢?可以通过举例子来理解,比方说我们现在要查询的区间是((1,8)),那么(k=3),从左端点查就是查((1,7)),从右端点查如果不(+1)的话,就是查((0,6)),所以是要(+1)的.再来严谨的讲一下,实际上,从右端点查询就是要找到一个点满足(x + 2^k - 1 = r),移项之后就是(x = r - 2^k + 1).
代码
#include <bits/stdc++.h>
using namespace std;
int maxx[1000010][50];
int ask(int x,int y){//查询
int k = log2(y - x + 1 );
return max(maxx[x][k],maxx[y - (1 << k) + 1][k]);
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++){
scanf("%d",&maxx[i][0]);
}
for(int i = 1;i <= 21;i++){//预处理
for(int j = 1;j + (1 << i) - 1 <= n;j++){
maxx[j][i] = max(maxx[j][i - 1],maxx[j + (1 << (i - 1))][i - 1]);
}
}
for(int i = 1;i <= m;i++){
int l,r;
scanf("%d%d",&l,&r);
printf("%d
",ask(l,r));
}
return 0;
}
另:
要注意在洛谷提交的时候一定要用(scanf),不能用(cin)否则会超时,同时要注意位运算的优先级,所以在预处理的时候那个括号很有讲究的(位运算的优先级是低于普通加减法的)