莫队 学习笔记

莫队 学习笔记

引入

莫队算法是一种优美的暴力算法,可以用来处理大量的区间询问。前提是,维护的信息可以高效地插入、删除。

我们就以一道题为例,来初探莫队:洛谷P3901 数列找不同

题意:给定一个数列,\(q\) 次询问,问区间 \([l, r]\) 内的数是否各不相同。

首先,我们很容易想到,问某个区间内的数各不相同,等价于去问这个区间的长度是否等于其中不同数的个数。那对于一个询问,我们先考虑暴力做:拿一个桶维护一下区间内每个数的个数,以及区间内不同种数的个数,从区间左侧扫到区间右侧,求出答案。但是,如果暴力去做,最后复杂度是 $q \times n $ 的,因为可能每次询问都要把整个数列扫描一遍。

我们又发现,其是我们并不需要每次都从头扫,因为有些区间的信息是重复的,那我们就要多利用重复的信息,于是就有了第二个思路:把询问离线下来,按左端点为第一关键字,右端点为第二关键字排序,用双指针来维护元素的插入和删除。这样,左端点的数就可以保证利用充分,但是,如果左端点各不相同,右端点还是会左右横跳,复杂度还是逃不过 \(q \times n\)

现在问题就变为,如何进行排序,来让每次左右端点尽量少移动,于是就有了莫队。

普通莫队

思想

莫队把区间分块,把询问以左端点所在的块为第一关键字,右端点为第二关键字进行排序,让我们来看一看这样做后的复杂度:

我们设块长为 \(size\)。首先,对于每个块内的询问,每次询问后左端点最多移动 \(size\) 次,那么所有询问后左端点移动的次数就是 \(q \times size\);然后我们来考察右端点。我们发现,在每个块内,右端点的移动都是单调不减的,可以做到线性;对于两个块之间的转换,右端点最多从最右侧移动到最左侧,也就是说,右端点最多移动 \(n \times \frac{n}{size}\) 次。又因为 \(n\)\(q\) 同阶,所以最终,莫队的复杂度就是 \(O(n \times size + n\times \frac{n}{size})\)

根据基本不懂不等式,我们发现当 \(size = \sqrt{n}\) 时,复杂度最小。所以按照 \(\sqrt{n}\) 分块,复杂度就变成 \(O(n \sqrt{n})\) 了。

一点点细节:左指针 \(l\) 初值最好赋成 \(1\),防止出现修改 \(a[0]\) 的情况(修改了不存在的值)。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;
inline int read(){
int x = 0; char ch = getchar();
while(ch<'0' || ch>'9') ch = getchar();
while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar();
return x;
}
int bel[N];
struct Question{
int l, r, id;
int part;
bool operator <(const Question &b) const{
if(part == b.part){
if(part & 1) return r<b.r;//玄学优化,按照奇偶性分别对右端点进行升/降序排序,让每次右端点回退最少。
else return r>b.r;
} else return part<b.part;
}
}q[N];
bool ans[N];
int a[N], n;
int Q;
int l = 1, r = 0;
int vis[N], tot;
void del(int pos){
vis[a[pos]]--;
if(!vis[a[pos]]) tot--;
}
void add(int pos){
if(!vis[a[pos]]) tot++;
vis[a[pos]]++;
}
int main(){
n = read(), Q = read();
int lth = sqrt(n);
for(int i = 1; i<=n; ++i) a[i] = read();
for(int i = 1; i<=Q; ++i){
q[i].l = read(), q[i].r = read();
q[i].id = i;
q[i].part = ((q[i].l-1)/lth)+1;
}
sort(q+1, q+Q+1);
for(int i = 1; i<=Q; ++i){
while(l<q[i].l) del(l++);
while(r>q[i].r) del(r--);
while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
ans[q[i].id] = (tot == (r-l+1));
}
for(int i = 1; i<=Q; ++i){
ans[i]?puts("Yes"):puts("No");
}
return 0;
}

例题

洛谷P4396 作业

我们发现两个询问都可以用树状数组维护,只不过维护的信息有所差异:一个维护存在与否,另一个维护数量。剩下的就是普通莫队了。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;
inline int read(){
int x = 0; char ch = getchar();
while(ch<'0' || ch>'9') ch = getchar();
while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar();
return x;
}
int n, m;
int a[N];
inline int lowbit(int x){
return x&(-x);
}
struct TC{//树状数组
int tc[N];
void insert(int pos, int val){
for(int i = pos; i<=n; i+=lowbit(i)){
tc[i]+=val;
}
}
int query(int pos){
int ret = 0;
for(int i = pos; i; i-=lowbit(i)){
ret+=tc[i];
}
return ret;
}
}ta, tb;//a:出现定义域内数的个数 b:出现定义域内数的种类数
struct Question{
int l, r, part, id;
int x, y;
bool operator < (const Question &b) const{
if(part == b.part){
if(part & 1) return r<b.r;
else return r>b.r;
} else{
return part < b.part;
}
}
}q[N];
int l = 1, r, vis[N];
void add(int x){
if(!vis[a[x]]) tb.insert(a[x], 1);//如果新加入一个数,存在性+1,删去同理
ta.insert(a[x], 1);//数量+1
vis[a[x]]++;
}
void del(int x){
vis[a[x]]--;
ta.insert(a[x], -1);
if(!vis[a[x]]) tb.insert(a[x], -1);
}
int ansa[N], ansb[N];
int main(){
n = read(), m = read();
for(int i = 1; i<=n; ++i) a[i] = read();
int lth = sqrt(n);
for(int i = 1; i<=m; ++i){
q[i].l = read(), q[i].r = read(), q[i].x = read(), q[i].y = read();
q[i].part = (q[i].l/lth)+1;
q[i].id = i;
}
sort(q+1, q+m+1);
for(int i = 1; i<=m; ++i){
while(r>q[i].r) del(r--);
while(r<q[i].r) add(++r);
while(l<q[i].l) del(l++);
while(l>q[i].l) add(--l);
ansa[q[i].id] = ta.query(q[i].y)-ta.query(q[i].x-1);
ansb[q[i].id] = tb.query(q[i].y)-tb.query(q[i].x-1);
}
for(int i = 1; i<=m; ++i){
printf("%d %d\n", ansa[i], ansb[i]);
}
return 0;
}

回滚莫队

在刚才的问题中,我们需要修改的信息往往都是易于撤销的,但是有时候,我们要维护一些添加容易删除难,或删除容易添加难的信息,该如何实现呢?

我们以歴史の研究为例。

题意:给定一个数列,和一些询问,每次询问区间内出现过的数与其出现次数乘积的最大值。

这里的最大值就是一个典型的便于添加不便于删除的信息,因为你每次的最大值都会覆盖上一次。怎么办呢?

我们就会想:能不能不删呢?也就是说,我们想办法让信息只添加不删除。

我们回到莫队上来。我们发现,排序的时候,每一个块内右端点是单调不减的,也就是说,这一部分可以实现只加不删;而左端点是乱序的。这时候就可以通过“回滚”来解决这一问题了:我们对于在同一个块内的左右端点,暴力求解;如果左右端点在不同的块,就让左右指针都指向左端点所在块的块尾(因为右端点一定在这个块之后),每次先移动右指针,添加信息,并记录,然后再移动左指针,添加信息。每个询问处理完后,都要把左指针更新的信息清空,答案恢复到左指针移动之前。

这样做的正确性有无保证呢?首先,如果是同一块内,暴力求肯定没问题;如果两个指针在不同的块,那么右指针只会单调不减向右移动来添加信息,所以对于左端点在同一个块内的询问,右指针的信息是不会被破坏的;我们再考虑左指针,每次通过回滚,就可以实现撤销操作。

关于复杂度,同一个块内是 \(\sqrt{n}\) 的,每次回滚也是 \(\sqrt{n}\) 的,所以复杂度其实没有变,变大的只是常数。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;
int n, Q;
int prt[N];
inline int read(){
int x = 0; char ch = getchar();
while(ch<'0' || ch>'9') ch = getchar();
while(ch>='0'&&ch<='9') x = x*10+ch-48, ch = getchar();
return x;
}
struct node1{
int x, id;
bool operator <(const node1 &b){
return x<b.x;
}
}ori[N];
int a[N], fa[N];
void init(){//离散化
sort(ori+1, ori+1+n);
int lst = 0, cnt = 0;
for(int i = 1; i<=n; ++i){
if(lst ^ ori[i].x) lst = ori[i].x, cnt++;
a[ori[i].id] = cnt;
fa[cnt] = ori[i].x;
}
}
struct Question{
int l, r, part, id;
bool operator < (const Question &b) const{
if(part == b.part) return r<b.r;
return part<b.part;
}
}q[N];
long long vis[N], ans[N];
long long visb[N];
int lp[N], rp[N];
int l = 1, r;
long long as1, as2;
void mo(){
int bl = 0;
for(int i = 1; i<=Q; ++i){
if(prt[q[i].l] == prt[q[i].r]){
for(int j = q[i].l; j<=q[i].r; ++j){//暴力求解,注意不要混用数组
visb[a[j]]++;
ans[q[i].id] = max(ans[q[i].id], visb[a[j]]*fa[a[j]]);
}
for(int j = q[i].l; j<=q[i].r; ++j){
visb[a[j]]--;
}
} else{
int now = prt[q[i].l];
if(bl ^ now){//换块后,要把上一个块询问的所有信息清空
as1 = 0;
for(int j = l; j<=r; ++j) vis[a[j]]--;
l = rp[now];
r = l-1;
bl = now;
}
while(r<q[i].r){
++r;
vis[a[r]]++;
as1 = max(as1, vis[a[r]]*fa[a[r]]);
}
as2 = as1;
while(l>q[i].l){
--l;
vis[a[l]]++;
as2 = max(as2, vis[a[l]]*fa[a[l]]);
}//移动左指针添加信息
ans[q[i].id] = as2;//记录答案
while(l<rp[now]){//清楚左指针的贡献
vis[a[l]]--;
++l;
}
}
}
}
int main(){
n = read(), Q = read();
int lth = sqrt(n);
for(int i = 1; i<=n; ++i) prt[i] = (i-1)/lth+1, ori[i].x = read(), ori[i].id = i;
init();
for(int i = 1; i<=Q; ++i){
q[i].l = read(), q[i].r = read();
q[i].part = prt[q[i].l];
q[i].id = i;
}
for(int i = 1; i<=prt[n]; ++i){
rp[i] = (i == prt[n]) ? n:i*lth;
}
sort(q+1, q+Q+1);
mo();
for(int i = 1; i<=Q; ++i){
printf("%lld\n", ans[i]);
}
system("pause");
return 0;
}
posted @   霜木_Atomic  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示