O(n)-O(1) 线性 RMQ 学习笔记
O(n)-O(1) 线性 RMQ 学习笔记
\(O(n)\) 预处理,\(O(1)\) 查询的 RMQ(区间最值)算法。
而我们正常 ST 表处理 RMQ 只能做到 \(O(n \log n)-O(1)\)。
用四毛子算法可以做到 \(O(n\log\log n)-O(1)\)。
四毛子算法:对原序列分块,块长为 \(O(\log n)\),块之间做 ST 表,每个块内做 ST 表(不能预处理前缀和后缀最值,因为可能有左右端点在一个块内的情况)。
但四毛子算法还是不够优秀。
那么下面我们介绍线性 RMQ 算法(也叫标准 RMQ 算法)。
1.建笛卡尔树
我们对原序列建出笛卡尔树,至于是大根还是小根依据具体题目。
我们可以使用单调栈 \(O(n)\) 建出笛卡尔树。
笛卡尔树的性质:区间 \([L,R]\) 最值位于笛卡尔树上 \(L,R\) 的 \(LCA\)。
于是区间最值转化为了求 \(LCA\)。
2.欧拉序转化 LCA
对笛卡尔树建出欧拉序。
欧拉序:初始一个空的序列,在 \(dfs\) 的过程中,每个点进入时将它加入序列末尾,离开时将它的父亲加入序列末尾,特别地,根节点离开时不加入它的父亲(它没有父亲),欧拉序是一个长为 \(2n-1\) 的序列。
欧拉序的性质:设 \(first_i\) 表示点 \(i\) 第一次在欧拉序中出现的位置,\(last_i\) 为最后一次出现的位置,则 \(i,j(first_i<first_j)\) 的 \(LCA\) 为欧拉序上 \([first_i,last_j]\) 区间内深度最小的点。
这是一个 \(\pm1\) RMQ 问题,也就是相邻两个元素的值刚好相差 \(1\)。
以上的主要思想就是把一般 RMQ 问题转化为 \(\pm1\) RMQ 问题。
3.\(\pm1\) RMQ 问题
做法 1
设 \(t\) 为欧拉序列的长度,即 \(t=2n-1\)。
我们采用 分块 + ST 表 + 散块状压 来解决这个问题,其中 分块 + ST 表 就是四毛子算法的思想。
取块长 \(B=O(\log n)\),对欧拉序列分块,则有 \(t/B\) 个块。
对于每个块之间做 ST 表,时间复杂度 \(O(t)\)。
而对于散点,我们并不能预处理每个块的前缀和后缀最值,因为可能出现左端点和右端点在同一个块里的情况。
我们对每个块的每个前缀以原数组的值维护一个单调栈,单调栈可以用 \(0/1\) 状压哪些元素在单调栈中,那么查询一个块的 \([l,r]\) 最值时,就是找前缀 \(r\) 的单调栈中,第一个下标大于等于 \(l\) 的元素,即第 \(l\) 位后第一个 \(1\)。
做法 2
考虑 CSP-S2021第一轮 中的做法。
\(t\) 还是如做法 1 的定义。
还是分块,块长我们取 \(B=\lceil\frac{\log t}{2}\rceil\),对于大块如做法 1。
而对于块内的 RMQ,一个块内的差分数组只有 \(2^{B-1}\) 种,预处理所有差分数组的最值位置,预处理复杂度 \(O(B2^{B-1})\),不超过 \(O(n)\),而查询就是 \(O(1)\) 了。
例题
直接用朴素的四毛子是过不了的。
我们可以考虑线性 RMQ,但这里提供一种不用线性 RMQ 的做法,常数较优秀。
考虑用一种另类的四毛子算法,对于散点用前缀和后缀最值处理。
然而对于两端点在同一个块内的情况,因为数据随机,所以两端点在一个块内的概率是 \(\frac{B}{n}\),此时直接暴力做是 \(O(B)\),同一块内的期望复杂度就是就是 \(O(\frac{B^2}{n})\)。
总的复杂度就是 \(O(n/B\log (n/B)+Q\frac{B^2}{n})\),当 \(n\) 与 \(Q\) 同阶时,\(B\) 取 \(\log n\) 到 \(\sqrt n\) 之间时,复杂度都是线性的。
建议取 \(\sqrt n\) 因为处理 ST 表很慢。
这道题的代码:
#include<bits/stdc++.h>
using namespace std;
namespace IO{
const int sz=1<<22;
char a[sz+5],b[sz+5],*p1=a,*p2=a,*t=b,p[105];
inline char gc(){
return p1==p2?(p2=(p1=a)+fread(a,1,sz,stdin),p1==p2?EOF:*p1++):*p1++;
}
template<class T> void read(T& x){
x=0; char c=gc(),fushu=0;
for(;c<'0'||c>'9';c=gc())if(c=='-')fushu=1;
for(;c>='0'&&c<='9';c=gc())
x=x*10+(c-'0');
if(fushu)x=-x;
}
inline void flush(){fwrite(b,1,t-b,stdout),t=b; }
inline void pc(char x){*t++=x; if(t-b==sz) flush(); }
template<class T> void write(T x,char c='\n'){
if(x<0) pc('-'), x=-x;
if(x==0) pc('0'); int t=0;
for(;x;x/=10) p[++t]=x%10+'0';
for(;t;--t) pc(p[t]); pc(c);
}
struct F{~F(){flush();}}f;
};
#define fo(i,l,r) for(int i=(l);i<=(r);++i)
#define fu(i,l,r) for(int i=(l);i<(r);++i)
#define fd(i,r,l) for(int i=(r);i>=(l);--i)
#define IOS ios::sync_with_stdio(0); cin.tie(0)
#define filein(x) freopen(x,"r",stdin)
#define fileout(x) freopen(x,"w",stdout)
#define usefile(x) freopen(x ".in","r",stdin); freopen(x ".out","w",stdout)
#define ll long long
#define ld long double
#define ull unsigned long long
//using IO::read;
//using IO::write;
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;
}
const int N=2e7+5;
int n,m,SEED;
ull ANS;
const int B=4472;
int a[N],b[N],c[N],ans[13][N/B+2],lg[B+10];
signed main(){
// usefile("rmq");
cin>>n>>m>>SEED;
srand(SEED);
fo(i,1,n){
a[i]=read();
int t=i/B;
if(a[i]>ans[0][t])ans[0][t]=a[i];
b[i]=a[i];
if((i-1)/B==i/B)b[i]=max(b[i-1],b[i]);
}
fd(i,n,1){
c[i]=a[i];
if((i+1)/B==i/B)c[i]=max(c[i+1],c[i]);
}
fo(i,2,n/B+1)lg[i]=lg[i>>1]+1;
fo(j,1,12)fo(i,0,n/B-(1<<j)+1)ans[j][i]=max(ans[j-1][i],ans[j-1][i+(1<<j-1)]);
fo(i,1,m){
int l=read()%n+1,r=read()%n+1;
if(l>r)swap(l,r);
int as=0;
if(l/B==r/B){
fo(j,l,r)as=max(as,a[j]);
}
else{
int L=l/B+1,R=r/B-1;
int len=R-L+1;
if(len>0)as=max(ans[lg[len]][L],ans[lg[len]][R-(1<<lg[len])+1]);
as=max(as,max(c[l],b[r]));
}
ANS+=as;
}
cout<<ANS;
return 0;
}