RMQ(Range Maximum(Minimum) Query)问题,即询问某个区间内的最大值或最小值,这里主要涉及RMQ问题的求解方法——ST算法。
简介
ST算法通常用于多次询问一些区间的最值的问题中。我们知道线段树可以O(mlogn)的解决RMQ问题(m是查询次数)。但是如果m很大(一般1e6以上),但是n比较小,就可以用ST算法解决RMQ问题,O(nlogn)预处理+O(1)查询。
算法流程
1.预处理
ST算法的原理实际上是动态规划,我们用a[1…n]表示一组数。设f[i][j]表示从a[i]到a[i+2 j-1]这个范围内的最大值,也就是以a[i]为起点连续2 j个数的最大值。由于元素的个数为2 j个,所以从中间平均分成两部分,每一部分的元素个数恰好为2 j-1个,也就是说,把f[i][j]分为f[i][j-1]和f[i+2 j-1][j-1]。
整个区间的最大值一定是左右两部分最大值的较大值,满足动态规划的最优化原理。转移方程为,f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1])。边界条件为f[i][0]=a[i]。这样就能在O(nlogn)的时间复杂度内预处理f数组。
2.询问
若我们要询问区间[l,r]的最大值,则先求出最大的x满足2 x<=r-l+1,那么区间[l,r]=[l,l+2 x-1]U[r-2 x+1,r]。
这里简单证明一下,这两个区间的并集就是[l,r]。首先此时2x<=r-l+1,所以l+2x-1<=r,r-2x+1>=l。所以不会比[l,r]大。那么会不会比[l,r]小内。利用反证法,我们这里假设比[l,r]小,那么r-2x+1>l+2x-1,我们移项,发现r-l+1>2x+1-1因为2x是小于等于r-l+1的最大的x,那么2x+1肯定大于r-l+1,所以r-l+1<=2x+1-1,与之前结论矛盾。那么就证明出来了。两区间并集就是[l,r]。
两区间的元素个数都为2
技巧
因为cmath库中的log2函数效率不高,所以除了调用log2函数外,通常还会使用O(n)递推预处理出1~n这n中区间长度各自对应的s值。具体地,log[d]表示log 2d向下取整,则log[d]=log[d/2]+1。
//ST算法O(nlogn)解决区间查询最大最小值问题(不能修改)
//cmath库自带log2函数效率不高,可以O(n)递推预处理log数组,之后可以O(1)查询
#include <iostream>
#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;
const int LogN=20;
int f[maxn][LogN+5];
int log[maxn];
int a[maxn];
int n,m,x,y;
int main()
{
scanf("%d%d",&n,&m);
log[0]=-1;//log[0]是-1才能使log[1]是0
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
for(int i=1;i<=n;++i)//O(n)预处理log数组
log[i]=log[i>>1]+1;
for(int i=1;i<=n;++i)
f[i][0]=a[i];
for(int j=1;j<=LogN;++j)
for(int i=1;i+(1<<j)-1<=n;++i)//边界不能超过n
f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);
while(m--)
{
scanf("%d%d",&x,&y);
int s=log[y-x+1];//求log(y-x+1)向下取整的值
printf("%d\n",max(f[x][s],f[y-(1<<s)+1][s]));
}
return 0;
}
最敏捷的机器人
也是模板题,只不过既要保存最大值,也要保存最小值。
#include <iostream>
#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
const int LogN=20;
const int maxn=1e6+5;
int n,k;
int log[maxn];
int f1[maxn][LogN+5];
int f2[maxn][LogN+5];
int a[maxn];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
log[0]=-1;
for(int i=1;i<=n;++i)
{
f1[i][0]=a[i];
f2[i][0]=a[i];
log[i]=log[i>>1]+1;
}
for(int j=1;j<=LogN;++j)
for(int i=1;i+(1<<j)-1<=n;++i)
{
f1[i][j]=max(f1[i][j-1],f1[i+(1<<j-1)][j-1]);
f2[i][j]=min(f2[i][j-1],f2[i+(1<<j-1)][j-1]);
}
for(int i=1;i+k-1<=n;++i)
{
int s=log[k];
printf("%d %d\n",max(f1[i][s],f1[i+k-1-(1<<s)+1][s]),min(f2[i][s],f2[i+k-1-(1<<s)+1][s]));
}
return 0;
}
与众不同
比较难的题目,我们可以O(n)推出last[],st[],f[]。last[value]表示value的之前最后出现位置。st[i]表示以i为结尾的最长完美序列的起点,st[i]=max(st[i-1],last[value]+1)。f[i]表示以i为结尾的最长完美序列长度,f[i]=i-st[i]+1。然后O(nlogn)求出mx[]。我们发现st[]是非递减的。那么一个区间可以分成两部分,前面部分的起点在l之前,后面部分的起点在l之后。然后由于是非递减的,就可以利用二分O(log)的求出分界点m。这样前面就是m-l。后面就是RMQ问题,max(f[m][s],f[r-(1<<s)+1][s])。
#include <iostream>
#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=2e5+5;
const int maxm=2e6+5;
const int LogN=20;
int n,m,x,y;
int a[maxn],last[maxm],st[maxn],f[maxn],mx[maxn][LogN];
int log[maxn];
int query(int l,int r)
{
if(st[l]==l)
return l;
if(st[r]<l)
return r+1;
int ans=r+1;
while(l<=r)
{
int mid=(l+r)>>1;
if(st[mid]>=x)
{
r=mid-1;
ans=min(ans,mid);
}
else
{
l=mid+1;
}
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
log[0]=-1;
for(int i=1;i<=n;++i)
{
st[i]=max(st[i-1],last[a[i]+1000000]+1);
f[i]=i-st[i]+1;
last[a[i]+1000000]=i;
mx[i][0]=f[i];
log[i]=log[i>>1]+1;
}
for(int j=1;j<=LogN;++j)
for(int i=1;i+(1<<j)-1<=n;++i)
mx[i][j]=max(mx[i][j-1],mx[i+(1<<j-1)][j-1]);
/*for(int i=1;i<=n;++i)
cout<<st[i]<<" ";
cout<<endl;
*/
int ans;
while(m--)
{
ans=0;
scanf("%d%d",&x,&y);
x++;
y++;
int m=query(x,y);
//cout<<m<<endl;
if(m>x)
ans=m-x;
if(m<=y)
{
int s=log[y-m+1];
ans=max(ans,max(mx[m][s],mx[y-(1<<s)+1][s]));
}
printf("%d\n",ans);
}
return 0;
}