RMQ介绍
要介绍RMQ算法首先要介绍其解决的问题,RMQ是一个解决多个区间最值查询的算法,RMQ(Range Minimum/Maximum Query),即区间最值查询,如果我们要查询某一个区间内的最值,显然我们可以用暴力搜索的方式在O(n)的时间复杂度内获得结果,但是当我们要查询10000个不同区间内的最值时,我们如果依然采用暴力搜索的方式,那么时间复杂度就会变成O(mn),其中m指的是查询次数,n指的是数据个数。
RMQ算法的目标就是降低多区间最值查询问题的时间复杂度,RMQ算法是通过动态规划思想实现的,他需要在O(nlogn)的时间复杂度内预处理出部分区间的最值,在O(1)的时间内获得一个区间的最值查询结果。总的时间复杂度被降到了O(nlogn)。
RMQ的思想是利用二进制组合的思想来对要求解问题进行优化的,我们知道计算机里面是采用二进制的数来存储所有数字的,那么如何能不漏的表示所有数字呢?
显然我们可以将所有自然数都对应一个二进制数,也即建立(0,0),(1,1),(10,2),(11,3)…的映射,其中映射中前面一部分表示二进制表示形式,而后面一种则是十进制表示形式,假设我们要对N个自然数进行表示,那么上述这种映射关系显然是O(n)的,那么我们是否可以减少二进制数的对应关系,通过二进制数的组合依然可以得到十进制数呢?显然是可以的,我们甚至可以只用一个二进制数字1来组合出所有的十进制数,但是值利用一个二进制数来表示所有的十进制数,组合过程反而变成了O(n)时间复杂度;
上述两种映射方式都是比较极端的情况,而我们的目标是找一个折中,目的是使表示和组合一个数的时间复杂度在多次达到最低。假设用来表示N个自然数的基底(也即二进制数目)有m个,要不漏的表示N个数,我们可以使用m个二进制数的各种组合来表示,那么最少需要多少二进制数才能不漏的表示所有数呢?显然要满足2^m=N也即m=logN,这样表示和查询的时间都是O(logn)的。
当然仅针对单个数,一一映射(实质上就是哈希)多次查询的时间复杂度是低于我们描述的第三种方法的,但是当一一映射指的是一个区间时,此时N个数可以形成的区间有N^2个,那么要建立一一映射的时间就是O(n ^2)的,而我们描述的第三种方式,建立映射的时间是O(nlogn)或者O(kn) ,多次查询的时间复杂度也是O(nk)或者O(nlogn)的,总的时间复杂度就是O(nlogn)的,因为k通常是一个不大的常数项。
与RMQ算法思想类似的是线段树,不过线段树建立时间是O(4n)的,查询时间是O(logn)的,RMQ算法建立时间是O(nlogn)的,查询时间反而是O(n)的,从中我们也可以看出,表示信息越详细,查询时间越短,但是表示信息越详细,时间复杂度也会越高,我们要从两者之中取到一个折中点。
ST算法思想
ST算法是解决区间最值查询的一种方法,类似的方法还有线段树。
按照上面描述的思想,我们使用dp[i][j]表示数组第i个元素开始,长度为2^j的区间内的最大值,这种表示方式恰好与上面描述的用最少的数据组合出所有可能区间的思想是一致的。
根据这种dp定义方式,我们可以轻松获得dp[i][j]的地推公式:
dp[i][j]=max(dp[i][j-1],dp[i+2^(j-1)][j-1]);
查询区间[l,r]之间的最大值时,令k=log(r-l+1),那么区间[l,r]之间的最大值=(dp[l][k]+dp[r-2^k+1][k]);这相当与取了两个较大区间的并集,以图示方式表示如下:
代码实现
#include<bits/stdc++.h>
using namespace std;
int main(){
std::ios::sync_with_stdio(false);
int n,m;
cin>>n>>m;
int num[n];
for(int i=0;i<n;i++)cin>>num[i];
//log(n)求得是以e为底的对数,所以我们要用换底公式lg2(n)=log(n)/log(2) ,或者世界使用log2(n)计算
int k=log2(n);
int dp[n][k+1];
memset(dp,0,sizeof(dp));
//dp[i][j]表示以i为起点,长度为2^j的区间中的最值。
for(int i=0;i<n;i++)dp[i][0]=num[i];//初始化
for(int i=1;(1<<i)<n;i++){//注意循环顺序,以及初始条件,加油
for(int j=0;j+(1<<i)<=n;j++){//注意边界条件啊
dp[j][i]=max(dp[j][i-1],dp[j+(1<<i-1)][i-1]);
}
}
for(int i=0;i<m;i++){
int l,r;
cin>>l>>r;
int d=log2(r-l+1);//区间长度
int ans=max(dp[l-1][d],dp[r-(1<<d)][d]);//两边查询
cout<<ans<<endl;//注意区间端点
}
return 0;
}