莫队
思考
为什么莫队常常与分块结合?因为分块是一个能做到 \(O(1)\) 插入、\(O(\sqrt{n})\) 查询(反过来也行)的数据结构。
而莫队是插入次数 \(O(n \sqrt{n})\),查询次数 \(O(m)\) 的。分块刚好可以平衡复杂度。
模板题 luogu - P2709 小B的询问
一开始给一个值域 \([1, k]\) 长度 \(n\) 的序列 \(a\)。
\(m\) 次询问,每次给出 \([l,r]\),求 \([l,r]\) 内所有数的出现次数的平方的和。
可以离线。
在这里介绍莫队。
先考虑一种枚举:依次处理询问,考虑从上一次询问区间,一步一步移动到 \([l,r]\)(即暴力枚举删掉哪些元素、添加哪些元素),这个过程里我们能不能维护出答案。
面对模板题,能不能呢?维护一个桶数组即可,毕竟有 \((x+1)^2 - x^2 = 2x + 1\) 和 \((x-1)^2 - x^2 = -2x + 1\),用桶数组即可维护答案。
然而这题可以离线,那就考虑离线(能离线的题就离线,显然离线永远比在线优秀)。
考虑对 \(m\) 次询问排序,使得 \(l,r\) 的变换次数尽可能小。考虑分块。对 \(l\) 分块,按照 \(l\) 所在的块排序,然后一个块内按照 \(r\) 排序。
每次 \(l\) 的变换次数 \(O(B)\),每个块内 \(r\) 的变换次数总和 \(O(n)\)。
时间复杂度 \(O(mB + \frac{n^2}{B})\),通过基本不等式最小值 \(2\sqrt{mn^2}\),\(mB = \frac{n^2}{B}\) 时取最小值,得 \(B = \sqrt{\frac{n^2}{m}}\)。
综上:\(B\) 取 \(\sqrt{\frac{n^2}{m}}\),时间复杂度 \(O(2n\sqrt{m})\)。
注意:莫队不像分块那样,要开大小有关 \(B\) 的数组,所以写莫队应该养成习惯用最优的 \(B\)。
注意:有的时候不支持在空集合里删除一个元素,这就要注意你莫队里四个 for 循环的顺序了。所以通常把插入的循环写在前面。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 5e4 + 3;
struct PII{
int first, second, id;
}ask[MAXN];
int n, m, k, a[MAXN];
LL ANS[MAXN];
int main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= m; i++){
cin >> ask[i].first >> ask[i].second, ask[i].id = i;
}
int B = sqrt(1ll * n * n / m);
sort(ask + 1, ask + 1 + m, [&](PII i, PII j){
if(i.first / B == j.first / B) return i.second < j.second;
return i.first / B < j.first / B;
});
LL ans = 0;
vector<int> sum(k + 3);
for(int q = 1, l = 1, r = 0; q <= m; q++){
while(ask[q].first < l) l--, ans += sum[a[l]] * 2 + 1, sum[a[l]]++;
while(ask[q].second > r) r++, ans += sum[a[r]] * 2 + 1, sum[a[r]]++;
while(ask[q].first > l) sum[a[l]]--, ans -= sum[a[l]] * 2 + 1, l++;
while(ask[q].second < r) sum[a[r]]--, ans -= sum[a[r]] * 2 + 1, r--;
ANS[ask[q].id] = ans;
}
for(int i = 1; i <= m; i++){
cout << ANS[i] << "\n";
}
return 0;
}
总结:当一个问题可以离线(且不带修),且可以使用那种枚举方法(上面说了),如果复杂度允许,优先考虑普通莫队(通常维护平方、不同颜色数这种东西,需要用到莫队)。
luogu - P5268 [SNOI2017] 一个简单的询问
二维差分,然后套莫队。
点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
using LL = long long;
const int MAXN = 5e4 + 5;
struct Qwe{
int l, r, id, w;
}q[MAXN * 4];
int n, m, d, _m = 0;
int a[MAXN];
LL S[3][MAXN], ans[MAXN];
bool cmp(Qwe i, Qwe j){
if(i.l / d == j.l / d){
return i.r < j.r;
}
return i.l / d < j.l / d;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
cin >> m;
for(int i = 1, l1, r1, l2, r2; i <= m; i++){
cin >> l1 >> r1 >> l2 >> r2;
q[++_m] = {r1, r2, i, 1};
q[++_m] = {l2 - 1, r1, i, -1};
q[++_m] = {l1 - 1, r2, i, -1};
q[++_m] = {l1 - 1, l2 - 1, i, 1};
}
for(int i = 1; i <= _m; i++){
if(q[i].l > q[i].r) swap(q[i].l, q[i].r);
}
d = sqrt(n), sort(q, q + _m, cmp);
for(int i = 1, l = 1, r = 1; i <= _m; i++){
for( ; l > q[i].l; ans[0] -= S[0][a[l]], S[1][a[l]]--, l--){
}
for( ; r > q[i].r; ans[0] -= S[1][a[r]], S[0][a[r]]--, r--){
}
for( ; l < q[i].l; l++, ans[0] += S[0][a[l]], S[1][a[l]]++){
}
for( ; r < q[i].r; r++, ans[0] += S[1][a[r]], S[0][a[r]]++){
}
ans[q[i].id] += 1ll * ans[0] * q[i].w;
}
for(int i = 1; i <= m; i++){
cout << ans[i] << "\n";
}
return 0;
}
luogu - P4689 [Ynoi Easy Round 2016] 这是我自己的发明
套 luogu - P5268 [SNOI2017] 一个简单的询问 即可。
luogu - P4137 Rmq Problem / mex
遇到 mex 想值域分块。
普通莫队即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 2e5 + 3, MAXM = 2e5 + 3;
struct Ask{
int l, r, tl, id;
}b[MAXM];
int n, m, B, _B;
int a[MAXN], sum[MAXN], mp[MAXN]; // 个数、模数
int lb[MAXN], rb[MAXN], ans[MAXN];
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m, B = 450, _B = 450;
for(int i = 0; i <= 2e5; i++) lb[i / B] = 1e9, rb[i / B] = 0;
for(int i = 0; i <= 2e5; i++) lb[i / B] = min(lb[i / B], i), rb[i / B] = max(rb[i / B], i);
for(int i = 1; i <= n; i++){
cin >> a[i];
}
for(int i = 1; i <= m; i++){
cin >> b[i].l >> b[i].r;
b[i].id = i, b[i].tl = b[i].l / B;
}
sort(b + 1, b + 1 + m, [](Ask i, Ask j){ return i.tl == j.tl ? i.r < j.r : i.tl < j.tl; });
for(int i = 1, l = 1, r = 0; i <= m; i++){
for( ; l < b[i].l; mp[a[l] / _B] -= (sum[a[l]] == 1), sum[a[l]]--, l++){
}
for( ; l > b[i].l; l--, mp[a[l] / _B] += (sum[a[l]] == 0), sum[a[l]]++){
}
for( ; r < b[i].r; r++, mp[a[r] / _B] += (sum[a[r]] == 0), sum[a[r]]++){
}
for( ; r > b[i].r; mp[a[r] / _B] -= (sum[a[r]] == 1), sum[a[r]]--, r--){
}
ans[b[i].id] = -1;
for(int g = 0; g <= 2e5 / B; g++){
int len = rb[g] - lb[g] + 1;
if(len > mp[g]){
for(int j = lb[g]; j <= rb[g]; j++){
if(sum[j] == 0){
ans[b[i].id] = j;
break;
}
}
if(ans[b[i].id] != -1) break;
}
}
}
for(int i = 1; i <= m; i++) cout << ans[i] << "\n";
return 0;
}
带修莫队 luogu - P1903 [国家集训队] 数颜色 / 维护队列
一开始给一个长度 \(n\) 的颜色序列,\(m\) 次操作。
- \(Q,L,R\) 问 \([L,R]\) 有多少种不同颜色。
- \(R,P,C\) 要把 \(P\) 的颜色改为 \(C\)。
可以离线。
有修改操作也可以莫队。
先看问题能不能用那种枚举方法,显然可以。
对 \(l\) 按块排序(第一关键字),对 \(r\) 按块排序(第二关键字),最后对 \(t\) 排序(第三关键字)。\(l\) 和 \(r\) 用同一个块长。
复杂度如何?我们考虑排序后,相邻两个询问之间的时间复杂度。设 \(M\) 表示 \(m\) 次操作中修改操作的次数。
- 首先每次 \(l\) 的时间复杂度都是 \(O(B)\),时间复杂度 \(O(mB)\)。
- 如果 \(l,r\) 的块都没变,\(t\) 的时间复杂度总共 \(O(M)\),\(r\) 的时间复杂度每次 \(O(B)\)。时间复杂度 \(O(\frac{n^2}{B^2}M + mB)\)
- 如果 \(l\) 的块没变,\(r\) 的块变了,\(t\) 的时间复杂度每次 \(O(M)\),\(r\) 的时间复杂度每次 \(O(B)\)。时间复杂度 \(O(\frac{n^2}{B^2}M + \frac{n^2}{B^2}B) \approx O(\frac{n^2}{B^2}M)\)。
- 如果 \(l\) 的块变了,时间复杂度 \(O(\frac{n}{B}(n + M))\)。
总复杂度约等于 \(O(\frac{n^2}{B^2}M + mB + \frac{n^2}{B})\)。
如果 \(M > B\),显然 \(\frac{n^2}{B^2} > \frac{n^2}{B}\),所以 \(\frac{n^2}{B^2}M = mB\) 的时候取最小值,得 \(B = \sqrt[3]{\frac{n^2M}{m}} \approx n^{\frac{2}{3}}\),时间复杂度 \(O(n^{\frac{2}{3}} (M + m))\)。
如果 \(M < B\),显然 \(\frac{n^2}{B^2} < \frac{n^2}{B}\),这就和普通莫队的复杂度一样了,\(B = \sqrt{\frac{n^2}{m}}\),时间复杂度 \(O(2n\sqrt{m})\)。
所以 \(M < \sqrt[3]{\frac{n^2M}{m}} \iff M < \frac{n}{\sqrt{m}}\) 时,时间复杂度就几乎和普通莫队一样了。
点击查看代码
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <vector>
#include <cmath>
using namespace std;
const int MAXN = 133333 + 5;
const int MAXV = 1e6 + 5;
struct pQwe{
int l, r, e, id, t;
}p[MAXN];
struct qQwe{
int x, w;
}q[MAXN];
int n, m, m1 = 0, m2 = 0, d;
int a[MAXN], ans[MAXN], S[MAXV];
char opt;
bool cmp1(pQwe i, pQwe j){
if(i.e == j.e){
if(i.r / d == j.r / d){
return i.t < j.t;
}
return i.r / d < j.r / d;
}
return i.e < j.e;
}
void C(int l, int r, int i){
if(l <= q[i].x && q[i].x <= r){
S[a[q[i].x]]--, ans[0] -= (S[a[q[i].x]] == 0);
ans[0] += (S[q[i].w] == 0), S[q[i].w]++;
}
swap(q[i].w, a[q[i].x]);
}
int main(){
//ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
d = pow(1.0 * n, 2.0 / 3);
for(int i = 1; i <= n; i++){
cin >> a[i];
}
for(int i = 1, L, R; i <= m; i++){
cin >> opt >> L >> R;
if(opt == 'R'){
m2++, q[m2] = {L, R};
}else{
m1++, p[m1] = {L, R, (L / d), m1, m2};
}
}
stable_sort(p + 1, p + 1 + m1, cmp1);
for(int i = 1, l = 1, r = 0, t = 0; i <= m1; i++){
for( ; t < p[i].t; t++, C(l, r, t)){
}
for( ; p[i].t < t; C(l, r, t), t--){
}
for( ; l < p[i].l; S[a[l]]--, ans[0] -= (S[a[l]] == 0), l++){
}
for( ; p[i].r < r; S[a[r]]--, ans[0] -= (S[a[r]] == 0), r--){
}
for( ; p[i].l < l; l--, ans[0] += (S[a[l]] == 0), S[a[l]]++){
}
for( ; r < p[i].r; r++, ans[0] += (S[a[r]] == 0), S[a[r]]++){
}
ans[p[i].id] = ans[0];
}
for(int i = 1; i <= m1; i++){
cout << ans[i] << "\n";
}
return 0;
}
总结:时间复杂度 \(O(n^{\frac{2}{3}} (M + m))\) 比较高,但 \(M\) 较小时会更快。
luogu - CF940F Machine Learning
考虑带修莫队。
注意到这题不用像 luogu - P4137 Rmq Problem / mex 一样值域分块,可以暴力枚举 mex,因为 mex 最大 \(\sqrt{n}\)。
点击查看代码
// LUOGU_RID: 148459543
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 2e5 + 3, B = 2154;
struct Ask{
int l, r, t, id;
}q[MAXN];
int n, m = 0, _m = 0, Q, a[MAXN], now[MAXN], vis[MAXN], mp[MAXN];
int ans[MAXN];
PII update[MAXN];
void ls(){ // 离散化
vector<int> p;
map<int, int> mp;
for(int i = 1; i <= n; i++) p.push_back(a[i]);
for(int i = 1; i <= _m; i++) p.push_back(update[i].second);
sort(p.begin(), p.end());
for(int i = 0, cnt = 0; i < p.size(); i++){
if(i == 0 || p[i] != p[i - 1]) cnt++, mp[p[i]] = cnt;
}
for(int i = 1; i <= n; i++) a[i] = mp[a[i]];
for(int i = 1; i <= _m; i++) update[i].second = mp[update[i].second];
}
vector<int> rch[MAXN];
void Change(int x, int y, int l, int r){
if(y == -1){
rch[x].pop_back(), y = rch[x].back();
}else{
rch[x].push_back(y);
}
if(l <= x && x <= r) mp[vis[now[x]]]--, vis[now[x]]--, mp[vis[now[x]]]++, mp[vis[y]]--, vis[y]++, mp[vis[y]]++;
now[x] = y;
}
int main(){
cin >> n >> Q;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
for(int i = 1, op, l, r; i <= Q; i++){
cin >> op >> l >> r;
if(op == 1){
m++, q[m] = {l, r, _m, m};
}else{
_m++, update[_m] = {l, r};
}
}
sort(q + 1, q + 1 + m, [](Ask i, Ask j){ return i.l/B == j.l/B ? (i.r/B==j.r/B?i.t<j.t:i.r/B<j.r/B) : i.l/B < j.l/B; });
ls();
for(int i = 1; i <= n; i++) rch[i].push_back(a[i]), now[i] = a[i];
for(int i = 1, l = 1, r = 0, t = 0; i <= m; i++){
for(; t < q[i].t; t++, Change(update[t].first, update[t].second, l, r)){
}
for(; t > q[i].t; Change(update[t].first, -1, l, r), t--){
}
for(; r < q[i].r; r++, mp[vis[now[r]]]--, vis[now[r]]++, mp[vis[now[r]]]++){
}
for(; l > q[i].l; l--, mp[vis[now[l]]]--, vis[now[l]]++, mp[vis[now[l]]]++){
}
for(; l < q[i].l; mp[vis[now[l]]]--, vis[now[l]]--, mp[vis[now[l]]]++, l++){
}
for(; r > q[i].r; mp[vis[now[r]]]--, vis[now[r]]--, mp[vis[now[r]]]++, r--){
}
for(int x = 1; ; x++){
if(mp[x] <= 0){
ans[q[i].id] = x;
break;
}
}
}
for(int i = 1; i <= m; i++) cout << ans[i] << "\n";
return 0;
}
回滚莫队 luogu - P5906 【模板】回滚莫队&不删除莫队
还有个模板题 [AT_joisc2014_c] 歴史の研究,但是那题能普通莫队通过,所以来做洛谷的模板题。
给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离。
序列中两个元素的间隔距离指的是两个元素下标差的绝对值。
考虑莫队。发现删除操作很难做。回滚莫队(不删除莫队)可以做到不用删除操作。
对于区间长度小于 \(B\) 的,暴力处理,时间复杂度 \(O(mB)\)。
枚举块,得到所有 \(l\) 在块中的 \([l,r]\),按照 \(r\) 排序,依次加入,对每个询问再暴力枚举 \(l\) 到块段点的元素。时间复杂度 \(O(mB + \frac{n^2}{B})\)。
总时间复杂度和普通莫队一样。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 2e5 + 3, MAXM = 2e5 + 3, MAXK = 2e5 + 3;
const int B = 450;
struct Ask{
int l, r, ans;
}q[MAXN];
int n, m, k = 0, now; // k 是不同的颜色个数
int col[MAXN];
int last[MAXK], finish[MAXK];
vector<int> p[B + 3];
inline int Retid(int x){ return (x - 1) / B + 1; }
void Insertr(int R){
finish[col[R]] = R;
last[col[R]] = (last[col[R]] == 0 ? R : last[col[R]]);
now = max(now, R - last[col[R]]);
}
void Insertl(int L, int qid){
if(finish[col[L]] == 0){
finish[col[L]] = L;
}
q[qid].ans = max(q[qid].ans, finish[col[L]] - L);
}
int main(){
cin >> n;
map<int, int> mpscol;
for(int i = 1; i <= n; i++){
cin >> col[i];
if(mpscol.find(col[i]) == mpscol.end()){ // 离散化
k++, mpscol[col[i]] = k;
}
col[i] = mpscol[col[i]];
}
cin >> m;
for(int i = 1; i <= m; i++){
cin >> q[i].l >> q[i].r;
if(Retid(q[i].l) == Retid(q[i].r)){ // 直接暴力处理
for(int j = q[i].l; j <= q[i].r; j++){
if(last[col[j]] > 0){
q[i].ans = max(q[i].ans, j - last[col[j]]);
}else last[col[j]] = j;
}
for(int j = q[i].l; j <= q[i].r; j++) last[col[j]] = 0;
}else{ // 待会处理
p[Retid(q[i].l)].push_back(i);
}
}
for(int d = 1; d <= Retid(n); d++){
sort(p[d].begin(), p[d].end(), [](int i, int j){ return q[i].r < q[j].r; });
int L = d * B, R = L - 1;
now = 0;
for(int id : p[d]){
for(; R < q[id].r; R++, Insertr(R)){
}
for(int l = L; l > q[id].l; l--, Insertl(l, id)){
}
q[id].ans = max(q[id].ans, now);
}
for(int c = 1; c <= k; c++) finish[c] = last[c] = 0;
}
for(int i = 1; i <= m; i++) cout << q[i].ans << "\n";
return 0;
}
总结:显然回滚莫队严格大于普通莫队,但是普通莫队更好写。
对于不好插入但好删除的问题,也能回滚莫队,对每个块的 \(r\) 改为从大到小枚举即可。
SP20644 ZQUERY - Zero Query
转为前缀和,然后和模板题一模一样。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 5e4 + 3, MAXM = 5e4 + 3, MAXK = 1e5 + 3;
const int B = 226;
struct Ask{
int l, r, ans;
}q[MAXN];
int n, m, k = 0, now; // k 是不同的颜色个数
int col[MAXN];
int M[2][MAXK];
vector<int> p[B + 10];
inline int Retid(int x){ return (x - 1) / B + 1; }
void Insertr(int R){
M[0][col[R-1]] = min(M[0][col[R-1]], R-1);
M[1][col[R]] = max(M[1][col[R]], R);
now = max(now, R - M[0][col[R]]);
}
void Insertl(int R, int qid){
M[1][col[R]] = max(M[1][col[R]], R);
q[qid].ans = max(q[qid].ans, M[1][col[R-1]] - R + 1);
}
int main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> col[i], col[i] += col[i - 1];
}
for(int i = 0; i <= n; i++) col[i] += 5e4;
for(int i = 0; i < MAXK; i++) M[0][i] = 1e9, M[1][i] = -1e9;
for(int i = 1; i <= m; i++){
cin >> q[i].l >> q[i].r;
if(Retid(q[i].l) == Retid(q[i].r)){ // 直接暴力处理
q[i].ans = 0;
for(int x = q[i].l; x <= q[i].r; x++){
M[0][col[x-1]] = min(M[0][col[x-1]], x-1);
q[i].ans = max(q[i].ans, x - M[0][col[x]]);
}
for(int x = q[i].l; x <= q[i].r; x++) M[0][col[x-1]] = 1e9;
}else{ // 待会处理
p[Retid(q[i].l)].push_back(i);
}
}
for(int d = 1; d <= Retid(n); d++){
sort(p[d].begin(), p[d].end(), [](int i, int j){ return q[i].r < q[j].r; });
int L = d * B, R = L - 1;
now = 0;
for(int id : p[d]){
for(; R < q[id].r; R++, Insertr(R)){
}
for(int l = L; l > q[id].l; l--, Insertl(l, id)){
}
q[id].ans = max(q[id].ans, now);
}
for(int i = 0; i < MAXK; i++) M[0][i] = 1e9, M[1][i] = -1e9;
}
for(int i = 1; i <= m; i++) cout << q[i].ans << "\n";
return 0;
}
莫队二次离线 luogu - P4887 【模板】莫队二次离线(第十四分块(前体))
考虑莫队,但这题又难插入又难删除。但是与其他问题的区别就是,这题加入点或删除点,计算答案不与当前区间有关。
即可以把莫队里的询问离线下来。
我们就考虑 \([l,r]\) 的 \(r\) 要增加的情况(其它情况同理)。从 \(r+1\) 枚举到 \(r'\),枚举到 \(x\) 就计算 \([l,x-1]\) 到 \(x\) 有多少贡献。可以对 \(l\) 扫描线,然后做个差分。
由于这题从 \(r\) 枚举到 \(r'\) 没有多余的复杂度,所以时间复杂度和普通莫队一样。
点击查看代码
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <vector>
#include <cmath>
using namespace std;
using LL = long long;
const int MAXN = 1e5 + 2;
const int MAXV = 16384;
struct Ask{
int l, r, li, id, ooo;
bool operator< (Ask j){
if(li == j.li) return r < j.r;
return li < j.li;
}
}q[MAXN];
int n, m, k, d;
int a[2][MAXN], vis[MAXV + 10], Sum[2][MAXN];
LL ans[MAXN];
vector<int> b;
vector<Ask> p[2];
void k_as(){ // 处理有 k 个 1 的数
fill(vis, vis + MAXV, 0);
if(0 == k) b.push_back(0);
for(int i = 1; i < MAXV; i++){
vis[i] = 1 + vis[i - (i & -i)]; // 减去最后的一个 1
if(vis[i] == k) b.push_back(i);
}
}
void ADD(int x){
for(int y : b) vis[x ^ y]++;
}
void P(int opt){ // 处理前缀贡献
fill(vis, vis + MAXV, 0);
for(int i = 1; i <= n; i++){
Sum[opt][i] = vis[a[opt][i]];
ADD(a[opt][i]);
}
}
void C(int opt){
sort(p[opt].begin(), p[opt].end());
fill(vis, vis + MAXV, 0);
for(int i = 1, j = 0; i <= n; i++){ // 扫描线
for( ; j < p[opt].size() && p[opt][j].li == i; j++){
for(int x = p[opt][j].l; x <= p[opt][j].r && x <= n; x++){
ans[p[opt][j].id] += (Sum[opt][x] - vis[a[opt][x]]) * p[opt][j].ooo;
}
}
ADD(a[opt][i]);
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; i++) cin >> a[0][i], a[1][n - i + 1] = a[0][i];
k_as(), P(0), P(1), d = 350;
for(int i = 1; i <= m; i++){
cin >> q[i].l >> q[i].r, q[i].id = i, q[i].li = q[i].l / d;
}
sort(q + 1, q + 1 + m);
for(int i = 1, l = 1, r = 0; i <= m; i++){ // 模板莫队
if(r < q[i].r){
p[0].push_back({r + 1, q[i].r, l, q[i].id, 1});
r = q[i].r;
}
if(q[i].l < l){
p[1].push_back({n - l + 2, n - q[i].l + 1, n - r + 1, q[i].id, 1});
l = q[i].l;
}
if(q[i].r < r){
p[0].push_back({q[i].r + 1, r, l, q[i].id, -1});
r = q[i].r;
}
if(l < q[i].l){
p[1].push_back({n - q[i].l + 2, n - l + 1, n - r + 1, q[i].id, -1});
l = q[i].l;
}
}
C(0), C(1);
for(int i = 1; i <= m; i++) ans[q[i].id] += ans[q[i - 1].id]; // 莫队的转移求和
for(int i = 1; i <= m; i++) cout << ans[i] << "\n";
return 0;
}
总结:如果一题你想要用莫队,但是发现又难删除又难插入时,可以考虑莫队二离(通常数 \((i,j)\) 对数的题需要莫队二离)
luogu - P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
普通莫队加个树状数组,\(O(n\sqrt{m} \log n)\),注意这题时间限制 250ms,无法通过。
注意到这题可以莫队二次离线。
我们用莫队二离就是为了省掉那个 \(\log n\)。有什么是我们能不用树状数组求出的?发现没有。树状数组是插入、查询都 \(\log n\),我们需要插入很慢,但查询 \(O(1)\) 的算法。
显然只有分块了。对值域分块即可。
直接和上题一样的做法即可(对 \(l\) 扫描线,然后做个差分)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 1e5 + 3;
struct Ask{
int l, r, id;
}ask[MAXN];
struct Work{
int il, l, r, id, op;
};
int n, m, a[MAXN];
LL ans[MAXN], num[MAXN], _num[MAXN];
vector<Work> S[2];
struct Block{
int B;
int sum[500], _sum[MAXN];
inline int Ri(int x){ return (x - 1) / B + 1; }
inline int Rl(int d){ return (d - 1) * B + 1; }
inline int Rr(int d){ return min(n, d * B); }
void ADD(int x, int w){
for(int d = Ri(x); d <= Ri(n); d++) sum[d] += w;
int d = Ri(x);
for(int i = x; i <= Rr(d); i++) _sum[i] += w;
}
int QUE(int x){
return sum[Ri(x) - 1] + _sum[x];
}
void CLE(){
for(int i = 1; i <= n; i++) _sum[i] = 0;
for(int i = 1; i <= Ri(n); i++) sum[i] = 0;
}
}block;
void Solve0(){
block.CLE();
sort(S[0].begin(), S[0].end(), [](Work i, Work j){ return i.il < j.il; });
for(int i = 1, j = 0; i <= n; i++){
while(j < S[0].size() && S[0][j].il == i){
for(int x = S[0][j].l; x <= S[0][j].r; x++){
ans[S[0][j].id] += S[0][j].op * (num[x] - (i - 1 - block.QUE(a[x])));
}
j++;
}
block.ADD(a[i], 1);
}
}
void Solve1(){
block.CLE();
sort(S[1].begin(), S[1].end(), [](Work i, Work j){ return i.il > j.il; });
for(int i = n, j = 0; i >= 1; i--){
while(j < S[1].size() && S[1][j].il == i){
for(int x = S[1][j].l; x <= S[1][j].r; x++){
ans[S[1][j].id] += S[1][j].op * (_num[x] - block.QUE(a[x] - 1));
}
j++;
}
block.ADD(a[i], 1);
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0);
//freopen("P5047_1.in", "r", stdin);
//freopen("test.out", "w", stdout);
cin >> n >> m, block.B = sqrt(n);
vector<int> vt;
for(int i = 1; i <= n; i++){
cin >> a[i], vt.push_back(a[i]);
}
sort(vt.begin(), vt.end());
int vtlen = unique(vt.begin(), vt.end()) - vt.begin();
while(vt.size() > vtlen) vt.pop_back();
for(int i = 1; i <= n; i++){
a[i] = lower_bound(vt.begin(), vt.end(), a[i]) - vt.begin() + 1;
}
for(int i = 1; i <= m; i++){
cin >> ask[i].l >> ask[i].r, ask[i].id = i;
}
int d = sqrt(1ll * n * n / m);
sort(ask + 1, ask + 1 + m, [&d](Ask i, Ask j){
if(i.l / d == j.l / d) return i.r < j.r;
return i.l / d < j.l / d;
});
for(int q = 1, l = 1, r = 0; q <= m; q++){
if(r < ask[q].r) S[0].push_back({l, r + 1, ask[q].r, ask[q].id, 1}), r = ask[q].r;
if(l > ask[q].l) S[1].push_back({r, ask[q].l, l - 1, ask[q].id, 1}), l = ask[q].l;
if(r > ask[q].r) S[0].push_back({l, ask[q].r + 1, r, ask[q].id, -1}), r = ask[q].r;
if(l < ask[q].l) S[1].push_back({r, l, ask[q].l - 1, ask[q].id, -1}), l = ask[q].l;
}
block.CLE();
for(int i = 1; i <= n; i++){
num[i] = (i - 1) - block.QUE(a[i]), block.ADD(a[i], 1);
}
block.CLE();
for(int i = n; i >= 1; i--){
_num[i] = block.QUE(a[i] - 1), block.ADD(a[i], 1);
}
Solve0(), Solve1();
for(int q = 1; q <= m; q++) ans[ask[q].id] += ans[ask[q - 1].id];
for(int q = 1; q <= m; q++) cout << ans[q] << "\n";
return 0;
}
luogu - P5398 [Ynoi2018] GOSICK(第十四分块)
做完两道基础题,来道第十四分块开开胃。
直接上莫队二次离线,考虑加入一个点贡献如何变化。
设 \(b_i\) 表示 \(i\) 的因数的出现次数,\(c_i\) 表示 \(i\) 的倍数的出现次数,加入一个 \(i\),贡献就增加 \(b_i + c_i\)。
一个数 \(x\) 的因数个数大概 \(O(\sqrt[3]{x})\),所以插入 \(x\) 暴力枚举因数,给 \(c_i\) 加一。
还要枚举倍数。考虑根号分治,如果 \(x > \sqrt{V}\) 暴力枚举倍数,给 \(b_i\) 加一。
问题是 \(x < \sqrt{V}\) 怎么办。
莫队二离不是有个差分吗?求 \([1,i-1]\) 加入 \(i\) 的新增贡献,和 \([1,i]\) 加入 \([l,r]\) 的新增贡献。
- 第一个:改为对 \(a_i\) 计算 \(< \sqrt{V}\) 的因数个数即可。
- 第二个:改为对 \(x < \sqrt{V}\) 的 \(x\) 计算 \([l,r]\) 中多少数是 \(x\) 的倍数。差分即可。
注意这题开不下一个 \(\sqrt{V} \cdot n\) 的数组,且这题空间 128MB,实现的时候注意点。
然后你写出来后发现 TLE 了,怎么卡都卡不进,面对这题丧心病狂的卡常,你不得不学习一种优秀实现:
- 我前面两道题都是暴力两个方向都做一遍。
- 但实际上不用,做一个方向即可。
- 你考虑 \(ql < l\) 的时候怎么算新增贡献。计算出 \([ql,l-1]\) 和 \([1,r]\) 的新增贡献,再减去 \([ql,l-1]\) 中所有 \(sum_i\)。
- 对 \(l < ql\) 同理。
你改完后发现还要卡常,那你就卡吧……fread 比普通快读快一些,还有 C++14 比 C++(GCC 9)要快。
下面这个代码用 C++14 过的,可能会有评测机波动导致这份代码 A 不了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
char buf[1<<20], *p1, *p2;
char gc() { return p1 == p2 ? p2 = buf + fread(p1 = buf, 1, 1<<20, stdin), (p1 == p2) ? EOF : *p1++ : *p1++; }
inline int read(int f = 1, char c = gc(), int x = 0) {
while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = gc();
while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = gc();
return f * x;
}
void write(LL x){ if(x>9) write(x/10); putchar(x%10+'0'); }
const int MAXN = 5e5 + 3;
int B;
struct Ask{
int l, r, id;
}ask[MAXN];
struct Node{
int il, l, r, id, op;
}S[MAXN * 2];
int n, m, a[MAXN], c[MAXN], b[MAXN], mp[MAXN];
LL ans[MAXN], sum[MAXN];
int __sum[MAXN];
int tot = 0;
vector<int> son[MAXN];
inline void ADD(int x){
for(int y : son[x]) c[y]++;
if(x <= B) mp[x]++;
else{
b[x]++;
for(int y = x; y <= 5e5; y += x) b[y]++;
}
}
int main(){
n = read(), m = read();
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= m; i++) ask[i].l = read(), ask[i].r = read(), ask[i].id = i;
int d = sqrt(1ll * n * n / m);
sort(ask + 1, ask + 1 + m, [&d](Ask i, Ask j){ return i.l / d == j.l / d ? i.r < j.r : i.l / d < j.l / d; });
for(int q = 1, l = 1, r = 0; q <= m; q++){
if(r < ask[q].r) S[++tot] = {l, r + 1, ask[q].r, ask[q].id, 1}, r = ask[q].r;
if(l > ask[q].l) S[++tot] = {r + 1, ask[q].l, l - 1, ask[q].id, -1}, l = ask[q].l;
if(r > ask[q].r) S[++tot] = {l, ask[q].r + 1, r, ask[q].id, -1}, r = ask[q].r;
if(l < ask[q].l) S[++tot] = {r + 1, l, ask[q].l - 1, ask[q].id, 1}, l = ask[q].l;
}
for(int i = 1; i <= 5e5; i++){
for(int j = i * 2; j <= 5e5; j += i) son[j].push_back(i);
}
// ========================================================
vector<LL> tmp(5e5 + 3);
for(int i = 1; i <= n; i++) tmp[a[i]] += int(5e5) / a[i];
for(int i = 5e5; i >= 1; i--) tmp[i] += tmp[i + 1];
LL now = 1e18;
for(int i = 1; i <= sqrt(n); i++){
if(1ll * i * n * 5 + tmp[i + 1] < now) now = 1ll * i * n * 5 + tmp[i + 1], B = i;
}
sort(S + 1, S + 1 + tot, [](Node i, Node j){ return i.il < j.il; });
for(int i = 1; i <= 5e5; i++) c[i] = b[i] = mp[i] = 0;
for(int i = 1, j = 1, x; i <= n + 1; i++){
sum[i] = sum[i - 1] + c[a[i]] + b[a[i]] + 1 + mp[a[i]] * 2;
for(int j : son[a[i]]){ if(j > B) break;
sum[i] += mp[j];
}
while(j <= tot && S[j].il == i){
for(x = S[j].l; x <= S[j].r; x++){
ans[S[j].id] -= (b[a[x]] + c[a[x]]) * S[j].op;
}
j++;
}
if(i == n + 1) break;
ADD(a[i]);
}
for(int j = 1; j <= tot; j++) ans[S[j].id] += (sum[S[j].r] - sum[S[j].l - 1]) * S[j].op;
// ====================================
for(int v = 1; v <= B; v++){
int cnt = 0;
for(int i = 1; i <= n; i++) __sum[i] = __sum[i - 1] + (a[i] % v == 0) + (a[i] == v);
for(int j = 1, i = 1; j <= tot; j++){
while(i < S[j].il) cnt += a[i] == v, i++;
ans[S[j].id] -= 1ll * cnt * (__sum[S[j].r] - __sum[S[j].l - 1]) * S[j].op;
}
}
for(int i = 1; i <= m; i++) ans[ask[i].id] += ans[ask[i - 1].id];
for(int i = 1; i <= m; i++) write(ans[i]), putchar('\n');
return 0;
}
树上莫队
SP10707 COT2 - Count on a tree II
不同颜色数?直接莫队。
但这题询问的不是子树,而是路径,难办。
有一种序列叫欧拉序(dfs 过程进入和退出 \(x\) 点,都给 stk 加入 \(x\))
对于一次询问 \(u,v\),哪些点在 \(u\) 到 \(v\) 的路径上?子树内只有 \(u\) 或只有 \(v\) 的点(再加上 \(lca(u,v)\))。
即欧拉序的 \([ed_u,st_v]\) 中只出现一次的点(再加上 \(lca(u,v)\))。
显然这个条件对我们做莫队没有影响(同样是删除或插入一点,只不过多了个判断条件而已)(但是不能回滚莫队了)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 4e5 + 3, MAXL = 20;
struct Ask{
int l, r, lca, id;
}q[MAXN];
int n, k = 0, Q, st[MAXN], ed[MAXN], pos[MAXN];
int col[MAXN], anc[MAXL][MAXN], dep[MAXN];
vector<int> eg[MAXN];
int ans[MAXN], sum[MAXN], vis[MAXN];
void Insert(int c){
ans[0] += sum[c] == 0, sum[c]++;
}
void Erase(int c){
ans[0] -= sum[c] == 1, sum[c]--;
}
void Update(int i){
if(vis[i] == 0){
vis[i] = 1, Insert(col[i]);
}else{
vis[i] = 0, Erase(col[i]);
}
}
void dfs(int x, int dad){
st[x] = ++k, pos[k] = x;
anc[0][x] = dad, dep[x] = dep[dad] + 1;
for(int nxt : eg[x]){
if(nxt != dad) dfs(nxt, x);
}
ed[x] = ++k, pos[k] = x;
}
int LCA(int x, int y){
if(dep[x] > dep[y]) swap(x, y);
for(int len = dep[y] - dep[x], l = 0; l < MAXL; l++){
if((len >> l) & 1) y = anc[l][y];
}
if(x == y) return x;
for(int l = MAXL - 1; l >= 0; l--){
if(anc[l][x] != anc[l][y]) x = anc[l][x], y = anc[l][y];
}
return anc[0][x];
}
void init(){
cin >> n >> Q;
map<int, int> mp;
for(int i = 1, x, cnt = 0; i <= n; i++){
cin >> x;
if(mp.find(x) == mp.end()) mp[x] = ++cnt;
x = mp[x], col[i] = x;
}
for(int i = 1, U, V; i < n; i++){
cin >> U >> V, eg[U].push_back(V), eg[V].push_back(U);
}
dfs(1, 0);
for(int l = 1; l < MAXL; l++){
for(int i = 1; i <= n; i++) anc[l][i] = anc[l-1][anc[l-1][i]];
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0);
init();
for(int i = 1, x, y; i <= Q; i++){
cin >> x >> y;
int lca = LCA(x, y);
if(lca == x || lca == y){
if(dep[x] > dep[y]) swap(x, y);
q[i].lca = -1, q[i].l = st[x], q[i].r = st[y], q[i].id = i;
continue;
}
if(ed[x] > st[y]) swap(x, y);
q[i].lca = lca, q[i].l = ed[x], q[i].r = st[y], q[i].id = i;
}
int B = sqrt(k);
B = max(1, B);
sort(q + 1, q + 1 + Q, [&B](Ask i, Ask j){
if(i.l / B == j.l / B){
return i.r < j.r;
}
return i.l / B < j.l / B;
});
for(int i = 1, l = 1, r = 0; i <= Q; i++){
for(; l > q[i].l; l--, Update(pos[l]));
for(; r < q[i].r; r++, Update(pos[r]));
for(; l < q[i].l; Update(pos[l]), l++);
for(; r > q[i].r; Update(pos[r]), r--);
if(q[i].lca > 0) Insert(col[q[i].lca]);
ans[q[i].id] = ans[0];
if(q[i].lca > 0) Erase(col[q[i].lca]);
}
for(int i = 1; i <= Q; i++){
cout << ans[i] << "\n";
}
return 0;
}
CF1479D - Odd Mineral Resource
这题还有个异或哈希做法,见异或哈希随笔 https://www.cnblogs.com/huangqixuan/p/19025467 。
考虑树上莫队。每次要在答案集中中插入或删除一个点,查询的时候分块即可。

浙公网安备 33010602011771号