ST算法
ST算法是基于倍增原理的,常用于求解最值区间查询(Range Minimum/Maximum Query,所以也成为RMQ问题),这是一种离线算法
1.算法原理
ST算法其实完全可以属于动态规划一类,因为它完全符合动态规划的最优子结构,即一个问题的最优解可以通过其子问题的最优解组合得到。
以最大值查询为例,一个大区间,如果能够被两个小区间覆盖,那么这个大区间的最值,一定等于这两个小区间的最值(无论最大值还是最小值),比如,有大区间{4,7,9,6,3,6},这个大区间完全被两个小区间{4,7,9,6,3},{6,3,6}覆盖,那么大区间的最大值就是两个小区间的最大值max(9,6)=9,并且不难看出,两个小区间的部分重合不影响效果,只需要保证两个小区间的并集完全等于大区间即可
2.实现步骤
- 预处理小区间
关于离线问题,一般都会先预处理,ST表预处理包括两个步骤:
(1) 把整个数列分为若干小区间,并且提前计算出每个小区间的最值
(2) 对任意区间最值查询,找到覆盖它的两个小区间,并由两个小区间的最值算出答案
小区间的划分可以借助倍增原理,即对数列每个元素,把从它开始的数列分为长度为1,2,4,8,16,32……的小区间
并且由算法原理可知,每组小区间的最值,可以由前一组递推而来,那么就可以定义dp[i][j]
表示左端点为 i ,区间长度为 2k 的区间的最值,那么预处理的递推关系式为dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][k-1])
,每个数字为左端点的各种长度都需要计算,那么一个数字需要计算 log2n 次,有 n 个数据,那么时间复杂度为 O(nlog2n)
- 查询任意区间最值
以任意元素为起点,后面有长度为1,2,4……的小区间,以任意元素为终点,前面亦有长度为1,2,4……的小区间
由此结论,可以将需要查询的区间 [L,R]
,分为两个小区间,一个是以L为起点的小区间,一个是以为R为重点的小区间,那么如何保证两个小区间的并集恰好等于查询区间呢?
区间 [L,R]
的长度为 len=R-L+1
,那么令两个小区间的长度均为 x
,那么由此得到限制 x<=len
,又因为我们预处理的数组中,用2的倍数来表示长度,所以 log2x <= log2len,由此性质我们可得,仅需要满足x
为 比len
小的2的最大倍数即可,所以 x=log2len,因为大量查询下,使用c++自带的log函数代码性能会显著下降,为此我们可以提前预处理log函数,
prelog[0]=-1;//这里要记得初始化,别忘了log函数在x趋近于0时,值趋近于负无穷
for(int i=1;i<=n;i++)
prelog[i]=prelog[i>>1]+1;
综上所述,可得区间查询公式dp[L][R]=max(dp[L][x],dp[R-(1<<x)+1][x])
,单次查询时间复杂度为 O(1)
3.模板代码(P3865 【模板】ST 表 && RMQ 问题 - 洛谷)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,m;
ll prelog[100005];
ll dp[100005][25];
void init(){
prelog[0]=-1;
for(int i=1;i<=n;i++)
prelog[i]=prelog[i>>1]+1;
ll len=prelog[n];
for(int j=1;j<=len;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
ll query(ll l,ll r){
ll len=r-l+1;
len=prelog[len];
return max(dp[l][len],dp[r-(1<<len)+1][len]);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>dp[i][0];
init();
while(m--){
ll l,r;
cin>>l>>r;
cout<<query(l,r)<<"\n";//此处小优化,endl每次输出会清空缓冲区,多次输出时效率会降低,所以此处采用 '\n'提高代码性能
}
return 0;
}