莫队学习笔记
前置知识
1、分块的基本思想(开根号等)
2、\(sort\) 的用法(手写 \(cmp\) 函数或重载运算符实现结构体的多关键字排序)
3、卡常
5、离散化(用于应付很多题目)
定义
一种优美的暴力算法,高效还好写。(考试出了个题才发现不会莫队)。其具体的操作大致就是把一个区间离线下来,后根据端点一点一点的跳,具体的过程可以见这篇博客。下边以例题来讲解莫队。
例题
莫队板子题。题目意思就是给你一段序列,每次询问一段区间,问其中有几种元素,那么如果暴力枚举的话肯定过不了,按照莫队的思想,我们每一次区间询问的时候,从上一个区间依次跳过来。跳每个点的时候,假如当前端点在查询的区间之外,那么每一次我们就让当前点的个数减一,如果减完,那么区间元素个数也就是答案就减一。同理,如果端点在区间内部,那么就需要往外跳,如果跳到一个点,它的值没有出现过,那么答案加一,出现过就直接让它的个数加一就行了。这样我们就解决了这道题。
代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
using namespace std;
const int L = 1 << 20;
char buffer[L],*S,*T;
#define gc (S == T && (T = (S = buffer) + fread(buffer,1,L,stdin),S == T) ? EOF : *S++)
#define read() ({int s = 0,f = 1;char ch = gc;for(;!isdigit(ch);ch = gc)if(ch == '-')f = -1;for(;isdigit(ch);ch = gc)s = s * 10 + ch - '0';s * f;})
const int maxn = 1e6+10;
int bel[maxn],block;
int a[maxn];
int cnt[maxn];
int sum,ans[maxn];
struct Node{
int l,r,id;
friend bool operator < (const Node& a,const Node& b){
return bel[a.l] == bel[b.l] ? a.r < b.r : bel[a.l] < bel[b.l];
}
}q[maxn];
inline void Del(int x){
cnt[a[x]]--;
if(!cnt[a[x]])sum--;
}
inline void Add(int x){
if(!cnt[a[x]])sum++;
cnt[a[x]]++;
}
int main(){
int n = read();
block = sqrt(n);
for(int i = 1;i <= n;++i){
a[i] = read();
bel[i] = (i - 1) / block + 1;
}
int m = read();
for(int i = 1;i <= m;++i){
q[i].l = read();q[i].r = read();
q[i].id = i;
}
sort(q+1,q+m+1);
int l = 1,r = 0;
for(int i = 1;i <= m;++i){
int ql = q[i].l,qr = q[i].r;
while(l < ql)Del(l++);
while(l > ql)Add(--l);
while(r < qr)Add(++r);
while(r > qr)Del(r--);
ans[q[i].id] = sum;
}
for(int i = 1;i <= m;++i){
printf("%d\n",ans[i]);
}
return 0;
}
Continue
当然,莫队不仅如此,如果仅仅是这样的话,那么这个算法也不能叫做高效暴力。假如我们的区间是 \([1,2]\),\([99999,100000]\) 交错进行,那么就不可行了。如果一直跳,肯定还不如直接跑暴力。所以我们来深入剖析一下莫队(大雾)。
1、预处理
莫队算法优化的核心是分块和排序。我们将大小为n的序列分为 \(\sqrt n\) 个块,编号,然后根据这个对查询区间进行排序。一种方法是把查询区间按照左端点所在块的序号排个序,如果左端点所在块相同,再按右端点排序。排完序后我们再进行左右指针跳来跳去的操作,虽然看似没多大用,但带来的优化实际上极大。复杂度 \(O(n\sqrt n)\)。证明也可以看上边那个博客。
由于我们需要对区间进行排序,所以莫队就不支持修改了,(当然有带修改莫队,但是我还不会)。
排序Code
struct Node{
int l,r,id;
friend bool operator < (const Node& a,const Node& b){
return bel[a.l] == bel[b.l] ? a.r < b.r : bel[a.l] < bel[b.l];
}
}q[maxn];
2、答案计算
莫队策略就是每跳一个距离就计算一下答案,所以在计算的时候,一般需要推一些式子,然后根据每一次的变化量等一些规律来进行答案的修改。
3、卡常
莫队通常是被卡的重灾区,如果想用莫队拿更多的分,有时候是需要卡常的。
*1、快读
最基本的,这里给一个特殊的(更nb……大雾)
const int L = 1 << 20;
char buffer[L],*S,*T;
#define gc (S == T && (T = (S = buffer) + fread(buffer,1,L,stdin),S == T) ? EOF : *S++)
#define read() ({int s = 0,f = 1;char ch = gc;for(;!isdigit(ch);ch = gc)if(ch == '-')f = -1;for(;isdigit(ch);ch = gc)s = s * 10 + ch - '0';s * f;})
*2、奇偶性排序
玄学奇偶性排序,优化巨大。它的主要原理便是右指针跳完奇数块往回跳时在同一个方向能顺路把偶数块跳完,然后跳完这个偶数块又能顺带把下一个奇数块跳完。理论上主算法运行时间减半,实际情况有所偏差。
struct Node{
int l,r,id;
friend bool operator < (const Node& a,const Node& b){
return (bel[a.l] ^ bel[b.l]) ? bel[a.l] < bel[b.l] : ((bel[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
}q[maxn];
*3、常数的缩小
有很多种方式,比如 \(register\) ,需要调用函数的写成宏定义或者直接写等等。
基础莫队差不多就这些了,写几个题印象和理解就会深刻许多了。
\(Never\ Give\ Up\)