0. 前言
以前只会 \(\mathcal O(n\log n)-\mathcal O(1)\) 的朴素 \(\rm rmq\)… 一度以为这就是 \(\rm rmq\) 的全部…
回想起来自己真是 \(\rm naive\) 得可爱。
放一道 \(\rm rmq\) 的题:由乃救爷爷,之后的内容应该就在这题上测了…
1. 前缀后缀分块
1.1. 算法流程
将序列划分成 \(\frac{n}{B}\) 个块,对于每个 \(i\),预处理出它在所属块的前缀 \(\max\) 与后缀 \(\max\)。再对块与块之间求一个朴素 \(\rm rmq\)。
查询就是出现散块就将前缀后缀拼在一起,整块就朴素 \(\rm rmq\)。注意还有 \(l,r\) 在同一块内的情况,这个直接暴力查询。
1.2. 复杂度分析
不太好分析的是 \(l,r\) 在同一块内的情况。枚举块共 \(\frac{n}{B}\) 种情况,那么 \(l,r\) 都在此块的概率为 \(\frac{B^2}{n^2}\),所以总概率是 \(\frac{B}{n}\),单次复杂度是 \(\mathcal O(\frac{B^2}{n})\)。所以总共是 \(\mathcal O(n+\frac{n}{B}\log \frac{n}{B}+q+q\cdot \frac{B^2}{n})\)。
当 \(B\) 至少为 \(\log n\) 时,\(\frac{n}{B}\log \frac{n}{B}\) 不超过 \(n\);当 \(B\) 至多为 \(\sqrt n\) 时,\(q\cdot \frac{B^2}{n}\) 不超过 \(n\)。
所以 \(B\) 取 \(\log n\sim \sqrt n\) 时即可。还有一个 出题人心理学分析,还蛮有意思的。
可以看作是 \(\mathcal O((n+q)\cdot \sqrt{\log n})\) 的。另外,听说块长设成 \(2^k\) 会快一些。
1.3. 代码
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f|=(s=='-');
while(s>='0' and s<='9')
x=(x<<1)+(x<<3)+(s^48),
s=getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-'),write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
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;
}
#include <iostream>
using namespace std;
const int B=64,maxn=20000005;
int n,m,pre[maxn],suf[maxn];
int st[20][maxn/B+2],bl[maxn];
int lg[maxn/B+2],a[maxn];
unsigned s;
inline int Max(const int a,const int b) { return a>b?a:b; }
int calc(int l,int r) {
if(l>r) swap(l,r);
int ret=0;
if(bl[l]^bl[r]) {
ret=Max(suf[l],pre[r]);
l=bl[l]+1,r=bl[r]-1;
if(l<=r) {
int d=lg[r-l+1];
ret=Max(ret,Max(st[d][l],st[d][r-(1<<d)+1]));
}
}
else {
for(int i=l;i<=r;++i)
ret=Max(ret,a[i]);
}
return ret;
}
void init() {
for(int i=1;i<=n;++i)
if((bl[i]-1)*B+1==i)
pre[i]=a[i];
else
pre[i]=Max(pre[i-1],a[i]);
suf[n]=a[n];
for(int i=n-1;i>=1;--i)
if(bl[i]*B==i)
suf[i]=a[i];
else
suf[i]=Max(suf[i+1],a[i]);
lg[0]=-1;
for(int i=1;i<=bl[n];++i)
lg[i]=lg[i>>1]+1,
st[0][i]=pre[i*B];
for(int j=1;(1<<j)<=bl[n];++j)
for(int i=1;i+(1<<j)-1<=bl[n];++i)
st[j][i]=Max(st[j-1][i],st[j-1][i+(1<<j-1)]);
}
int main() {
n=read(9),m=read(9),s=read(9);
srand(s);
for(int i=1;i<=n;++i)
a[i]=read(),
bl[i]=(i-1)/B+1;
int l,r; init();
unsigned long long ans=0;
while(m--) {
l=read()%n+1;
r=read()%n+1;
ans+=calc(l,r);
}
print(ans,'\n');
return 0;
}
2. 压位分块
2.1. 算法流程
将 \(B\) 设为 \(\log n\),对块与块之间求一个朴素 \(\rm rmq\)。对每个块从左到右维护递减的单调栈,注意由于将状态压缩成一个数字,我们要求字长 \(w\ge B\)。具体一点就是如果某个下标在单调栈内,就将数字对应那一位赋值为 \(1\)。查询块内 \((l,r)\) 时就查询 \(r\) 对应的单调栈状态满足 \(i\ge l\) 的第一个为 \(1\) 的数字即可。可以用 __builtin_ctz()
在 \(\mathcal O(1)\) 内计算。
2.2. 复杂度分析
朴素 \(\rm rmq\) 只用 \(\frac{n}{\log n}\) 个数,复杂度 \(\mathcal O(n)\)。单调栈预处理 \(\mathcal O(n)\),查询 \(\mathcal O(q)\)。
但是模板题还不如前缀后缀分块跑得快就是了。可能是我人丑常数大吧。
2.3. 代码
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f|=(s=='-');
while(s>='0' and s<='9')
x=(x<<1)+(x<<3)+(s^48),
s=getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-'),write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
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;
}
inline int Max(int x,int y) {int m=(x-y)>>31; return y&m|x&~m;}
#include <iostream>
using namespace std;
const int B=32,maxn=20000005;
int n,m,l,r,S[B+5],tp;
int a[maxn],lg[maxn/B+2];
int st[20][(int)9e5];
unsigned s;
struct Stack {
int sta[B+5],delta;
int QMax(int l,int r) {
return a[delta+l+__builtin_ctz(sta[r]>>l-1)];
}
void ins(int *A) {
tp=0;
for(int i=1;i<=B;++i) {
sta[i]=sta[i-1];
while(tp and A[S[tp]]<=A[i])
sta[i]^=(1<<S[tp]-1),--tp;
S[++tp]=i,sta[i]^=(1<<i-1);
}
}
} p[(int)9e5];
void init() {
int len=(n-1)/B+1; lg[0]=-1;
for(int i=1;i<=len;++i) {
st[0][i]=a[(i-1)*B+1];
for(int j=(i-1)*B+2;j<=i*B;++j)
st[0][i]=Max(st[0][i],a[j]);
lg[i]=lg[i>>1]+1;
p[i].ins(a+(i-1)*B),p[i].delta=(i-1)*B;
}
for(int j=1;(1<<j)<=len;++j)
for(int i=1;i+(1<<j)-1<=len;++i)
st[j][i]=Max(st[j-1][i],st[j-1][i+(1<<j-1)]);
}
int rmq(int l,int r) {
int d=lg[r-l+1];
return Max(st[d][l],st[d][r-(1<<d)+1]);
}
int calc() {
int a=(l-1)/B+1,b=(r-1)/B+1,ret=0;
if(a+1<=b-1) ret=rmq(a+1,b-1);
if(a==b) ret=p[a].QMax(l-(a-1)*B,r-(a-1)*B);
else ret=Max(ret,Max(p[a].QMax(l-(a-1)*B,B),p[b].QMax(1,r-(b-1)*B)));
return ret;
}
int main() {
n=read(9),m=read(9),s=read(9);
srand(s);
for(int i=1;i<=n;++i)
a[i]=read();
init();
unsigned long long ans=0;
while(m--) {
l=read()%n+1,r=read()%n+1;
if(l>r) swap(l,r);
ans+=calc();
}
print(ans,'\n');
return 0;
}
3. \(±1\text{ rmq}\)
听说时空都不太优,所以只放一个 别人的博客。按理来说时间复杂度应该是 \(\mathcal O(n)-\mathcal O(1)\) 的。
4. 笛卡尔树
4.1. 算法流程
建好笛卡尔树后(小根堆),如果要查询 \([l,r]\) 的最小值就是求 \(l,r\) 在树上的 \(\rm lca\)。然后可以暴力跳到 \(\rm lca\)。
4.2. 复杂度分析
由于在 随机数据 下,笛卡尔树的树高期望是 \(\log n\) 的,所以应该是 \(\mathcal O(n)-\mathcal O(\log n)\)。