1. 算法简介
莫队算法有很多种:普通莫队,带修莫队,回滚莫队,树上莫队,二维莫队,莫队二次离线。
莫队算法主要用于解决支持快速插入,删除贡献的区间优化问题。
具体的,对于要求解贡献的区间 \([l,r]\) 来说,我们可以把以前求解过的区间 \([L,R]\) 的贡献保留下来,并通过移动 \(L,R\),同时插入,删除贡献的方式得到新区间 \([l,r]\) 的贡献。
比如要求解区间内出现次数最大的数。
可以先维护一个桶,记录每一次数出现的次数。然后对于询问区间 \([l,r]\),将指针 \(L,R\) 移动至 \([l,r]\) 处。同时将移动时摒弃掉的贡献删去,将新增的贡献插入。
2. 算法理论
可以发现如果直接按照上述操作进行的话,时间复杂度会退化至 \(O(n)\)。原因是每一次操作最坏会移动 \(n\) 步。
考虑如何优化,将原序列分块,所有询问离线存储。然后将区间左端点按其所在的块的编号作为第一关键字排序,按区间右端点为第二关键字排序。再进行上述操作,便可以做到 \(O((n+m)\sqrt n)\)。
证明如下:
左端点要么在块内移动,要么每次跨越一个块。共进行 \(m\) 轮,时间复杂度为 \(O(m\sqrt n)\)。
右端点在左端点的同一个块内只会向前移动,最多移动 \(n\) 步。时间复杂度 \(O(n\sqrt n)\)。
综上,时间复杂度为 \(O((n+m)\sqrt n)\)。十分的巧妙。
小 trick:奇偶性排序,最坏情况下右端点每次跑完一遍就会回到最前面,这样极其浪费时间。不妨将区间左端点的块编按照奇偶分类。第二关键字排序时,奇数块的从小到大,偶数块从大到小。这样可以省去很多的时间。
以下是奇偶性排序的代码,其中 \(p_i\) 表示 \(i\) 号元素的块编:
bool cmp(Node x, Node y) {
return p[x.l] == p[y.l] ? (p[x.l] & 1 ? x.r < y.r : x.r > y.r) : p[x.l] < p[y.l];
}
3. 各色莫队
3.1 普通莫队
3.1.1 P1972 [SDOI2009] HH的项链
以 P1972 [SDOI2009] HH的项链 为例。
经典数颜色。
维护一个桶,然后创建一个区间指针 \(l=0,r=1\) 分别指向当前区间左右端点。
然后进行区间转移:
while(l < q[i].l) del(l++);
while(l > q[i].l) add(--l);
while(r < q[i].r) add(++r);
while(r > q[i].r) del(r--);
然后是插入和删除函数:
void add(int x) {
ans += 2 * cnt[a[x]] + 1;
cnt[a[x]]++;
}
void del(int x) {
ans += (-2 * cnt[a[x]]) + 1;
cnt[a[x]]--;
}
然后就可以 \(O(n\sqrt n)\) 通过部分分数,原因是这道题卡了莫队的做法。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e6 + 10;
struct Node {
int l, r, i;
} q[N];
int n, m, a[N], B, cnt[N], ans, Ans[N];
bool cmp(Node a, Node b) {
return (a.l / B) ^ (b.l / B) ? a.l < b.l : ((a.l / B) & 1 ? a.r < b.r : a.r > b.r);
}
void add(int x) {
if(!cnt[a[x]]) ans++;
cnt[a[x]]++;
}
void del(int x) {
cnt[a[x]]--;
if(!cnt[a[x]]) ans--;
}
signed main() {
n = read();
For(i,1,n) a[i] = read();
m = read();
B = n/sqrt(m*2/3);
For(i,1,m) q[i] = (Node){read(), read(), i};
sort(q + 1, q + m + 1, cmp);
int l = 0, r = 0;
For(i,1,m) {
int ql = q[i].l, qr = q[i].r;
while(ql < l) add(--l);
while(ql > l) del(l++);
while(qr < r) del(r--);
while(qr > r) add(++r);
Ans[q[i].i] = ans;
}
For(i,1,m) {
cout << Ans[i] << '\n';
}
return 0;
}
3.1.2 P2709 小B的询问
考虑拆贡献:插入贡献时。\(c_i^2\ce{->}(c_i + 1)^2=c_i^2+2c_i+1\),于是原答案上更新 \(+2c_i+1\) 即可。删除贡献同理。
然后又变成数颜色的题了。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N = 5e4 + 10;
struct Node {
int l, r, id;
} q[N];
int n, m, k, cnt[N], a[N], p[N], L, ans, Ans[N];
bool cmp(Node x, Node y) {
return p[x.l] == p[y.l] ? (p[x.l] & 1 ? x.r < y.r : x.r > y.r) : p[x.l] < p[y.l];
}
void add(int x) {
ans += 2 * cnt[a[x]] + 1;
cnt[a[x]]++;
}
void del(int x) {
ans += (-2 * cnt[a[x]]) + 1;
cnt[a[x]]--;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
L = sqrt(n);
For(i,1,n) cin >> a[i], p[i] = (i - 1) / L + 1;
For(i,1,m) cin >> q[i].l >> q[i].r, q[i].id = i;
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
For(i,1,m) {
while(l < q[i].l) del(l++);
while(l > q[i].l) add(--l);
while(r < q[i].r) add(++r);
while(r > q[i].r) del(r--);
Ans[q[i].id] = ans;
}
For(i,1,m) cout << Ans[i] << '\n';
return 0;
}
3.2 带修莫队
在普通莫队上增加了一个修改操作。
处理方法同普通莫队一样,将修改操作的时间戳记下,当成莫队的一维。这样,每一次修改的情况就有六种:
- \([l,r-1,t]\);
- \([l,r+1,t]\);
- \([l+1,r,t]\);
- \([l-1,r,t]\);
- \([l,r,t+1]\);
- \([l,r,t-1]\);
操作的排序是按照左端点所在块为第一关键字,右端点所在块为第二关键字,时间戳为第三关键字。
修改操作的细节:只有修改在本次操作区间的贡献才会对当前答案造成影响,但是同样会影响到原序列,所以每一次修改要判断修改的贡献是否在操作区间中,同时修改原系列的答案。
注意,块长设为 \(n^{\frac{2}{3}}\) 时,时间复杂度最优。
3.2.1 P1903 [国家集训队] 数颜色 / 维护队列
带修莫队板子。
同样是维护一个桶,没什么细节,时间也非常宽裕。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
using namespace std;
const int N = 1e6 + 10;
struct Node {
int l, r, t, id;
} q[N];
struct node {
int x, y;
} upd[N];
int n, m, Q, a[N], L, p[N], T, Ans[N], cnt[N], ans;
bool cmp(Node x, Node y) {
return (p[x.l] == p[y.l] ? (p[x.r] == p[y.r] ? x.t < y.t : p[x.r] < p[y.r]) : p[x.l] < p[y.l]);
}
void add(int x) {
if(!cnt[x]) ans++;
cnt[x]++;
}
void del(int x) {
cnt[x]--;
if(!cnt[x]) ans--;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
L = pow(n, 2.0 / 3.0);
For(i,1,n) cin >> a[i], p[i] = (i-1) / L + 1;
For(i,1,m) {
char op; int x, y;
cin >> op >> x >> y;
if(op == 'Q') {
q[++Q] = (Node){x, y, T, Q};
} else {
upd[++T] = (node){x, y};
}
}
sort(q + 1, q + Q + 1, cmp);
int l = 1, r = 0, t = 0;
For(i,1,Q) {
while(l > q[i].l) add(a[--l]);
while(l < q[i].l) del(a[l++]);
while(r < q[i].r) add(a[++r]);
while(r > q[i].r) del(a[r--]);
while(t < q[i].t) {
++t;
if(q[i].l <= upd[t].x && upd[t].x <= q[i].r) {
del(a[upd[t].x]);
add(upd[t].y);
}
swap(a[upd[t].x], upd[t].y);
}
while(t > q[i].t) {
if(q[i].l <= upd[t].x && upd[t].x <= q[i].r) {
del(a[upd[t].x]);
add(upd[t].y);
}
swap(a[upd[t].x], upd[t].y);
--t;
}
Ans[q[i].id] = ans;
}
For(i,1,Q) cout << Ans[i] << '\n';
return 0;
}