莫队

思考

为什么莫队常常与分块结合?因为分块是一个能做到 \(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

考虑树上莫队。每次要在答案集中中插入或删除一个点,查询的时候分块即可。

posted @ 2025-08-08 11:00  hhhqx  阅读(82)  评论(0)    收藏  举报