莫队 学习笔记
莫队 学习笔记
引入
莫队算法是一种优美的暴力算法,可以用来处理大量的区间询问。前提是,维护的信息可以高效地插入、删除。
我们就以一道题为例,来初探莫队:洛谷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; }
例题
我们发现两个询问都可以用树状数组维护,只不过维护的信息有所差异:一个维护存在与否,另一个维护数量。剩下的就是普通莫队了。
#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; }