0. 前言
以前只会 (mathcal O(nlog n)-mathcal O(1)) 的朴素 ( m rmq)… 一度以为这就是 ( m rmq) 的全部…
回想起来自己真是 ( m naive) 得可爱。
放一道 ( m rmq) 的题:由乃救爷爷,之后的内容应该就在这题上测了…
1. 前缀后缀分块
1.1. 算法流程
将序列划分成 (frac{n}{B}) 个块,对于每个 (i),预处理出它在所属块的前缀 (max) 与后缀 (max)。再对块与块之间求一个朴素 ( m rmq)。
查询就是出现散块就将前缀后缀拼在一起,整块就朴素 ( m 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+qcdot frac{B^2}{n}))。
当 (B) 至少为 (log n) 时,(frac{n}{B}log frac{n}{B}) 不超过 (n);当 (B) 至多为 (sqrt n) 时,(qcdot frac{B^2}{n}) 不超过 (n)。
所以 (B) 取 (log nsim 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,'
');
return 0;
}
2. 压位分块
2.1. 算法流程
将 (B) 设为 (log n),对块与块之间求一个朴素 (
m rmq)。对每个块从左到右维护递减的单调栈,注意由于将状态压缩成一个数字,我们要求字长 (wge B)。具体一点就是如果某个下标在单调栈内,就将数字对应那一位赋值为 (1)。查询块内 ((l,r)) 时就查询 (r) 对应的单调栈状态满足 (ige l) 的第一个为 (1) 的数字即可。可以用 __builtin_ctz()
在 (mathcal O(1)) 内计算。
2.2. 复杂度分析
朴素 ( m 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,'
');
return 0;
}
3. (±1 ext{ rmq})
听说时空都不太优,所以只放一个 别人的博客。按理来说时间复杂度应该是 (mathcal O(n)-mathcal O(1)) 的。
4. 笛卡尔树
4.1. 算法流程
建好笛卡尔树后(小根堆),如果要查询 ([l,r]) 的最小值就是求 (l,r) 在树上的 ( m lca)。然后可以暴力跳到 ( m lca)。
4.2. 复杂度分析
由于在 随机数据 下,笛卡尔树的树高期望是 (log n) 的,所以应该是 (mathcal O(n)-mathcal O(log n))。