前言
发现自己忘记了ST表然后搞了一发就来学RMQ了。
注:接下来的时间复杂度标记方式为\(\text{O}{(数据预处理)} \sim \text{O}{(单次询问)}\)
简介
- RMQ是英文 Range Maximum/Mininmum Query 的缩写,表示区间最大(最小)值
算法实现
ST表
-
基于倍增思想。不支持修改
-
在我看来是个DP,f[i][j]用来记录从i开始跳\(1<<j\)步达到的查询
-
时间复杂度\(\text{O}{(n \log n)} \sim \text{O}{(1)}\)
-
空间复杂度\(\text{O}{(n \log n)}\)
int n,m,lg[25];
int f[N][25];
inline int query(int l,int r){
int k=log2(r-l+1);
return max(f[l][k],f[r-lg[k]+1][k]);
}//可以覆盖整个区间
int main(){
n=read();m=read();lg[0]=1;
for(int i=1;i<=21;i++)lg[i]=lg[i-1]<<1;
for(int i=1;i<=n;i++)f[i][0]=read();
for(int k=1;k<=21;k++)
for(int i=1;i+lg[k]-1<=n;i++){
f[i][k]=max(f[i][k-1],f[i+lg[k-1]][k-1]);
}
for(int i=1,l,r;i<=m;i++){
l=read();r=read();
printf("%d\n",query(l,r));
}
return 0;
}
除此之外,ST表还可以维护其他的可重复贡献问题。
例题
线段树
- 时间复杂度\(\text{O}{(n)} \sim \text{O}{( \log n)}\)
- 空间复杂度\(\text{O}{(n)}\)
Four Russian
-
不愧是战斗民族发明出这么
恶臭的暴力的算法:) -
在ST表的基础上进行序列分块。将每一块建立一个ST表,解决问题时通过ST表上的区间查询解决。
-
在\(block=\log n\)时,预处理复杂度达到最优,为\(\text{O}{(n \log \log n)}\)
但在算法竞赛中一般将块大小设为\(\sqrt {n}\),然后预处理出每一块内前缀和后缀的RMQ,再暴力预处理出任意连续的整块直接的RMQ,时间复杂度为\(\text{O}{(n)}\)。
- 对于左右端点不在同一块内的询问,我们可以直接\(\text{O}{1}\)得到左端点所在块的后缀RMQ,左端点和右端点之间的连续整块RMQ,和右端点所在块的前缀RMQ,答案即为三者最值
- 对于左右端点在同一块内的询问,我们可以暴力求出两点间的RMQ,时间复杂度为\(\text{O}{(n)}\),但是单个询问左右端点在同一块内的期望为\(\text{O}{\frac{\sqrt{n}}{n}}\),所以这种方法的时间复杂度为期望\(\text{O}{(n)}\)。
而在算法竞赛中,我们并不用非常担心出题人卡掉这种算法,因为我们可以通过在\(\sqrt {n}\)的基础上随机微调块大小,很大程度上避免算法在根据特定块大小构造的数据中出现最坏情况。并且如果出题人想要卡掉这种方法,则暴力有可能可以通过。
————noip(毒瘤)
例题
由乃救爷爷
我终于写出了四毛子,必须贴!!
code
#include<bits/stdc++.h>
using namespace std;
namespace GenHelper{
unsigned z1,z2,z3,z4,b;
unsigned rand_(){
b=((z1<<6)^z1)>>13;
z1=((z1&4294967294U)<<18)^b;
b=((z2<<2)^z2)>>27;
z2=((z2&4294967288U)<<2)^b;
b=((z3<<13)^z3)>>21;
z3=((z3&4294967280U)<<7)^b;
b=((z4<<3)^z4)>>12;
z4=((z4&4294967168U)<<13)^b;
return (z1^z2^z3^z4);
}
}
void srand(unsigned x){
using namespace GenHelper;
z1=x;
z2=(~x)^0x233333333U;
z3=x^0x1234598766U;
z4=(~x)+51;
}
int read(){
using namespace GenHelper;
int a=rand_()&32767;
int b=rand_()&32767;
return a*32768+b;
}
typedef unsigned long long uLL;
const int N=20000005;
const int maxb=4473,sumb=maxb+1005;
inline int input(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return x*f;
}
int n,m,s,a[N+maxb];
int block,lim;
int f[sumb][maxb],lg[27],highbit[N+maxb];
int L[sumb][maxb],R[sumb][maxb];
uLL ans=0;
int main(){
n=input();m=input();s=input();
srand(unsigned(s));lg[0]=1;
for(int i=1;i<=26;i++)lg[i]=lg[i-1]<<1;
block=sqrt(n);
lim=(n-1)/block;
for(int i=0;i<n;i++){
a[i]=read();
f[i/block][0]=max(f[i/block][0],a[i]);
}
for(int i=lim;i>=0;i--)
for(int k=1;i+lg[k]-1<=lim;k++)
f[i][k]=max(f[i][k-1],f[i+lg[k-1]][k-1]);
for(int i=0;i<=lim;i++){
int now=i*block;
L[i][0]=a[now];
for(int k=1;k<block;k++){
L[i][k]=max(L[i][k-1],a[now+k]);
}
R[i][block-1]=a[now+block-1];
for(int k=block-2;k>=0;k--)
R[i][k]=max(R[i][k+1],a[now+k]);
}
for(int k=0;lg[k]<=n;k++)
for(int i=lg[k];i<lg[k+1];i++){
if(i>n)break;
highbit[i]=k;
}
for(int i=1;i<=m;i++){
int l=read()%n,r=read()%n;
if(l>r)swap(l,r);
int bl=l/block,br=r/block;
int now=0;
if(bl==br){
for(int i=l;i<=r;i++)
now=max(now,a[i]);
}else {
now=max(now,R[bl][l%block]);
now=max(now,L[br][r%block]);
int len=br-bl-1;
int k=highbit[len];
if(len){
now=max(now,f[bl+1][k]);
now=max(now,f[br-lg[k]][k]);
}
}
ans+=unsigned(now);
}
printf("%llu",ans);
return 0;
}
加减1RMQ
-
若序列满足相邻两元素相差为 1,在这个序列上做 RMQ 可以成为加减 1RMQ
-
由于相邻两个数字的差值为\(1\),所以在固定左端点数字时长度不超过$ \log n\(的右侧序列种类数为\)\sum_{i=1}^{i \le \log n}2^{i-1}\(,而这个式子显然不超过\)n\(。可以预处理出所以不超过\)n$种情况的最小值-第一个元素的值。
-
我们可以用它来优化四毛子预处理
- 预处理同一块内相邻两个数字直接的差,并且用二进制将其表示出来,在询问时找到询问区间对应的二进制表示查表得出答案
预处理时间可以优化到\(O(n)\)
笛卡尔树
- 将普通RMQ转换为LCA问题,时间复杂度为\(\text{O}{(n)} \sim \text{O}{(\log n)}\),常数小但会被卡
- 可以进而转化为加减1RMQ问题,时间复杂度为\(\text{O}{(n)} \sim \text{O}{1}\),常数大
code
int n,m,s,a[N];
int top,sta[N];
int ls[N],rs[N];
unsigned long long ans;
inline int query(int x,int l,int r){
while(x<l||x>r)
x=x<l?rs[x]:ls[x];
return x;
}
int main(){
n=input();m=input();s=input();srand(s);
for(int i=1;i<=n;i++){
a[i]=read();
int k=top;
while(k&&a[i]>a[sta[k]])k--;
if(k)rs[sta[k]]=i;
if(k<top)ls[i]=sta[k+1];
sta[++k]=i;top=k;
}
int root=sta[1];
for(int i=1,l,r;i<=m;i++){
l=read()%n+1;r=read()%n+1;
if(l>r)swap(l,r);
ans+=a[query(root,l,r)];
}
printf("%llu",ans);
return 0;
}
这是直接暴力查找的,大概就这个意思。是由乃救爷爷那个题的。因为暴力查找只能过随机化的数据啊悲
基于状压的线性RMQ做法
请巨佬们自己研究吧!!
练习
板子题
- ST表和暴力查询的笛卡尔树都能水过
- 给定年份和降雨量问“X年是自Y年以来降雨量最多的”这句话必真、必假还是有可能
- map映射,小细节多
下面是一些例题
例题
一、由乃救爷爷
-
求RMQ以及询问,\(n,m \le 20000000\)。看这个数据范围用ST表直接GG。
-
两种做法:
-
\(\sqrt{n}\)分块:预处理时间复杂度\(\text{O}{(n)}\),查找时在同一块内的期望为\(\text{O}{(\frac{\sqrt{n}}{n})}\),所以暴力查找的时间复杂度为\(\text{O}{(n)}\),其他的查找为\(\text{O}{(1)}\)。所以时间复杂度为\(\text{O}{(n)}\)。空间复杂度\(\text{O}{(4n)}\)不会炸。
这种方法并不好卡,因为卡该算法\(\sqrt{n}\)的暴力时有可能会让真正的暴力通过
-
笛卡尔树:因为是随机数据,所以可以直接暴力查找。预处理\(O(n)\),查找\(\text{O}{(\log n)}\)的,跑得飞快(指快过分块)。空间复杂度\(\text{O}{(4n)}\)不会炸。
-
-
求长为a,宽为b的矩形边长为n的min(RMaxQ-RMinQ)。\(2 \le n \le m,n \le a,n \le b,n \le 100\)。
-
单调队列优化的二维RMQ(雾)
-
暴力的想法很简单,用ST表,每行求RMQ,然后枚举。
-
在暴力的想法之上,仍旧是枚举每行每列,但是可以先枚举每列然后枚举行,利用单调队列来维护行上在要取的\(n\)为边长正方形内的Max和Min即可
->其实直接用单调队列一枚到底(指在行上也直接用单调队列维护预处理)也行。
-
老经典题了大多数人都做过,但题太少了所以……(雾)
-
贪心+堆维护最优
-
用RMQ记录下标,找[l,r]最大的子段和的下标
-
然后用大根堆每次取堆顶然后再扔进去[l,pos-1],[pos+1,r](pos是最优的位置)取k次即可
-
求解\(\sum_{i=l}^{r}\sum_{j=i+1}^{r} \space \min{a[i \sim j]}\)
-
发现题解里好多用莫队的,还有因为随机数据笛卡尔树和线段树莽的,还有各种神奇数据结构的

- 实际上这个题的莫队就是普通莫队只是用来简单的优化查询而已,其他的用数据结构的是在线做法,比如什么线段树扫描线啊猫树啊奇怪的树状数组啊,对比之下莫队不就香香了嘛。(反正大家都会普通莫队……的吧)
- 莫队实现主要在于如何维护[l,r+1] (知道这个了自然也就知道[l-1,r],[l,r-1],[l+1,r]了)。发现本次增加的是[l,r+1],[l+1,r+1]...[r+1,r+1],考虑[l,r+1]之间最小值的位置为p。则[l,p]的最小值一点为a[p],贡献为\(a[p]\times (p-l+1)\),可以用RMQ来解决。
- [p+1,r+1]内的数一定全部比a[p]大,所以一定有向左第一个比当前小的a[x]的位置为p。 设f[l] [r]表示以r为右端点,左端点在[l,r]的区间的答案。记L[i]表示从i向前第一个比i小的数的位置,则左端点在[L[r],r]的区间最小值都是a[r],所以\(f[l][r]=f[l][L[r]]+a[r]\times(r-L[r])\),发现与l无关可以去掉这一维,即\(f[r]=f[L[r]]+a[r]\times(r-L[r])\)。 又知道\(L[x],x\in[p+1,r+1]\)至少有一个p,所以一定可以计算出f[p+1] [r+1]
- \(f_{r+1}=a_{r+1}\times (r+1-pre_{r+1})+...+a_x\times (x-p)+f_p\)。所以\(f_{r+1}-f_p\)就是要求的f[p+1] [r+1]
五、水の数列
事实证明毒瘤题都是多次强化出来的(雾
- 给定一个长度为\(N\)的数列,选择\(x\),将小于等于\(x\)的数标记,且得到的区间个数在\(l \sim r\)范围内,得到的得分是每一个连续标记区间的长度的平方和/x,T组询问,强制在线
- \(1 \le N \le 10^6,1 \le T \le 10^3,1 \le num_i \le 10^6\)
- 这……只能通过x来预处理所有情况了……吧?记录每个x出现的位置,用len维护扩展的两端的长度再记录val用来更新当前x的ans即可。
- 查询的时候通过RMQ查询分块[l,r]内的最大即可,但因为块的个数不可能超过\(n>>1\)个所以ST表可以开下。