莫队学习笔记

· · 算法·理论

膜拜给本蒟蒻讲莫队的大佬。

普通莫队

原理

可行条件

  1. 离线

步骤

把原数组 a 分成 \sqrt{n} 块。

把询问离线,以 l 所在块为第一关键字、r 所在块为第二关键字排序。遍历询问从上一个暴力更新到下一个。

int m = q.size();
vector<T> res(m);
T ans = 0;
for (int i = 0, l = 1, r = 0; i < m; i++) {
  while (l > q[i].l) add(--l, ans);
  while (r < q[i].r) add(++r, ans);
  while (l < q[i].l) del(l++, ans);
  while (r > q[i].r) del(r--, ans);
  res[q[i].id] = ans;
}

正确性显然,主要是复杂度。

结论:n,q 同阶,取块长 \sqrt{n} 时,复杂度为 O(n\sqrt{n}+q \log q)

### 复杂度证明 排序显然为 $O(q \log q)$。 对于 $l$ 的移动:下一个询问与 $l$ 所在块不同时,$d_{\max}=2\sqrt{n}$。 对于 $r$ 的移动:记 $i$ 所属的块为 $b_i$。若 $b_r \ne b_{r'}$ 退化为暴力,复杂度 $O(n)$。同块 $b_i$ 相同,此时排序使用第二关键字 $r$,由于 $r$ 单增,故 $r$ 的移动次数是 $O(n)$ 的。共有 $\dfrac{n}{\sqrt{n}}=\sqrt{n}$ 个块,所以 $r$ 的移动是 $O(n\sqrt{n})$ 的。 $O(\dfrac{n^2}{m}+mq)$ 的证明类似。 ### 优化 #### 块长优化 当 $q$ 远大于 $n$ 时,需要使 $\dfrac{n^2}{m}+mq$ 最小,此时取块长 $m=\dfrac{n}{\sqrt{q}}$。 #### 奇偶排序优化 从证明中可知,$r$ 的移动是很慢的。 对于属于奇数块的询问,$r$ 按从小到大排序,对于属于偶数块的排序,$r$ 从大到小排序。 这样 $r$ 先处理奇数再处理偶数,是有效的常数优化。 ### 代码模板 ```cpp template <class T> class MBlock { private: int unit; struct node { int l, r, id; node() : l(0), r(0), id(0) {} node(int a, int b, int c) : l(a), r(b), id(c) {} }; public: vector<node> q; explicit MBlock(int u) : unit(u) {} void add_query(int l, int r, int id) {q.emplace_back(l, r, id);} vector<T> get(const function<void(int, T&)>& add, const function<void(int, T&)>& del, const function<void(T&, int)>& get = [](T&, int){}) { sort(q.begin(), q.end(), [&](const node& x, const node& y) -> bool { if (x.l / unit != y.l / unit) return x.l < y.l; if ((x.l / unit) & 1) return x.r < y.r; return x.r > y.r; }); int m = q.size(); vector<T> res(m); T ans = 0; for (int i = 0, l = 1, r = 0; i < m; i++) { while (l > q[i].l) add(--l, ans); while (r < q[i].r) add(++r, ans); while (l < q[i].l) del(l++, ans); while (r > q[i].r) del(r--, ans); get(ans, q[i].id), res[q[i].id] = ans; } return res; } }; ``` ## 例题 ### [P2709 小B的询问](https://www.luogu.com.cn/problem/P2709) 莫队板子之一。代码如下: ```cpp int n, q, k, l, r, a[N], cnt[N]; void _main() { read(n, q, k); read(a + 1, a + n + 1); MBlock<int> mblock(sqrt(n)); for (int i = 0; i < q; i++) { read(l, r); mblock.add_query(l, r, i); } vector<int> res = mblock.get( [&](int pos, int& t) -> void { t += 2 * cnt[a[pos]] + 1; cnt[a[pos]]++; }, [&](int pos, int& t) -> void { t -= 2 * cnt[a[pos]] - 1; cnt[a[pos]]--; } ); for (int i : res) writeln(i); } ``` ### [P1494 [国家集训队] 小 Z 的袜子](https://www.luogu.com.cn/problem/P1494) 首先你需要特判 $l=r$,修改一下 `get` 函数即可。 然后令区间内 $i$ 中颜色的数量为 $a_i$,则分子贡献为 $\dfrac{1}{2}a_i(a_i+1)$,分母是 $\dfrac{1}{2}(r-l+1)(r-l)$,然后直接输出即可。 ```cpp int n, q, l, r, a[N], cnt[N], ll[N], rr[N]; void _main() { read(n, q); read(a + 1, a + n + 1); MBlock<int> mblock(sqrt(n)); for (int i = 0; i < q; i++) { read(l, r); mblock.add_query(l, r, i); } mblock.init(); vector<int> res = mblock.get( [&](int pos, int& t) -> void {t += cnt[a[pos]]++;}, [&](int pos, int& t) -> void {t -= --cnt[a[pos]];} ); for (int i = 0; i < q; i++) { ll[mblock.q[i].id] = mblock.q[i].l; rr[mblock.q[i].id] = mblock.q[i].r; } for (int i = 0; i < q; i++) { if (res[i] == 0) write(0), putchar('/'), writeln(1); else { int t = (rr[i] - ll[i] + 1) * (rr[i] - ll[i]) / 2; int g = __gcd(res[i], t); write(res[i] / g), putchar('/'), writeln(t / g); } } } ``` ### [P4137 Rmq Problem / mex](https://www.luogu.com.cn/problem/P4137) 其实这题应该放在回滚莫队里,但是 bitset 优化就冲过去了…… `del` 是平凡的,对于 `add`,用一个 `bitset` 压位存储下一个数字是否出现,暴力找即可。复杂度为 $O(n\sqrt{n}+\dfrac{qV}{w})$,其中 $V$ 为值域,$w=64$。开 O2 即可冲过。 ```cpp int n, q, a[N], l, r, cnt[N]; bitset<N> bs; void add(int x, int& res) { cnt[a[x]]++, bs[a[x]] = 0; if (res == a[x]) res = bs._Find_next(a[x]); } void del(int x, int& res) { if (--cnt[a[x]] == 0) { bs[a[x]] = 1; res = min(res, a[x]); } } ``` ### [P4462 [CQOI2018] 异或序列](https://www.luogu.com.cn/problem/P4462) / [CF617E XOR and Favorite Number](https://www.luogu.com.cn/problem/CF617E)。 先处理出来前缀异或 $pre$。由 $pre_i \bigoplus k=pre_j$ 得 $pre_i =pre_j\bigoplus k$,所以开个桶记录一下更新即可。 由于前缀异或的式子是 $pre_j \bigoplus pre_{i-1}$,所以左端点要减一。 ```cpp #define int long long int n, q, k, a[N], pre[N], cnt[N], l, r; void _main() { cin >> n >> q >> k; for (int i = 1; i <= n; i++) cin >> a[i], pre[i] = pre[i - 1] ^ a[i]; MBlock<int> mblock(sqrt(n)); for (int i = 0; i < q; i++) { cin >> l >> r; mblock.add_query(l - 1, r, i); } vector<int> res = mblock.get( [&](int x, int& res) -> void { res += cnt[pre[x] ^ k]; cnt[pre[x]]++; }, [&](int x, int& res) -> void { cnt[pre[x]]--; res -= cnt[pre[x] ^ k]; } ); for (int i : res) cout << i << '\n'; } ``` ### [P3709 大爷的字符串题](https://www.luogu.com.cn/problem/P3709) 题意翻译:查询 $[l,r]$ 区间的众数的出现次数的相反数。 出现次数开通维护即可,众数开桶の桶维护。 但是本蒟蒻懒得离散化,使用 `cc_hash_table` 吸着氧气 920ms 水过。 ```cpp #include <bits/extc++.h> using namespace __gnu_pbds; int n, q, a[N], l, r, num[N]; cc_hash_table<int, int> cnt; void _main() { cin >> n >> q; for (int i = 1; i <= n; i++) cin >> a[i]; MBlock<int> mblock(sqrt(n)); for (int i = 0; i < q; i++) { cin >> l >> r; mblock.add_query(l, r, i); } vector<int> res = mblock.get( [&](int x, int& res) -> void { num[cnt[a[x]]]--, num[++cnt[a[x]]]++; res = max(res, cnt[a[x]]); }, [&](int x, int& res) -> void { num[cnt[a[x]]]--; if (cnt[a[x]] == res && !num[cnt[a[x]]]) res--; num[--cnt[a[x]]]++; } ); for (int i : res) cout << -i << '\n'; } ``` # 带修莫队 ## 原理 ### 可行条件 这里我们记 $t$ 为这次查询之前做了多少次修改。 1. 离线 2. $[l,r,t]$ 的答案可以以较低复杂度转移到 $[l-1,r,t],[l+1,r,t],[l,r-1,t],[l,r+1,t],[l,r,t-1],[l,r,t+1]$。 ### 步骤 把数组分成 $O(n ^{\frac{1}{3}})$ 块,块长 $O(n ^{\frac{2}{3}})$。 > 注:最优块长应当是 $O(\dfrac{n ^{\frac{2}{3}} t^ {\frac{1}{3}}}{q ^{\frac{2}{3}}})$。证明见 [OI-Wiki](https://oi-wiki.org/misc/modifiable-mo-algo/)。实际应用中,一般取 $O(n ^{\frac{2}{3}})$。 类似普通莫队地,坐标除了 $l,r$ 的移动,在时间轴 $t$ 上也作移动。 认为 $n,q,t$ 同级,可以得出,复杂度为 $O(n^{\frac{5}{3}})$,相比于暴力 $O(n^2)$ 还是有很大优化的。 ### 代码模板 相较于普通莫队,这里新增了 `upd` 函数。 ```cpp template <class T> class MBlock { private: int unit, time; struct node { int l, r, t, id; node() : l(0), r(0), t(0), id(0) {} node(int a, int b, int c, int d) : l(a), r(b), t(c), id(d) {} }; struct change { int l; T v; change() : l(0), v(0) {} change(int a, const T& b) : l(a), v(b) {} }; public: vector<node> qq; vector<change> qr; explicit MBlock(int u) : unit(u), time(0) {} void add_query(int l, int r, int id) {qq.emplace_back(l, r, time, id);} void add_change(int l, const T& v) {time++, qr.emplace_back(l, v);} vector<T> get(const function<void(int, T&)>& add, const function<void(int, T&)>& del, const function<void(int, int, T&)>& upd, const function<void(T&, int)>& get = [](T&, int){}) { sort(qq.begin(), qq.end(), [&](const node& x, const node& y) -> bool { if (x.l / unit == y.l / unit) { if (x.r / unit == y.r / unit) return x.t < y.t; return x.r < y.r; } return x.l < y.l; }); int m = qq.size(); vector<T> res(m); T ans = 0; for (int i = 0, l = 1, r = 0, t = 0; i < m; i++) { while (l > qq[i].l) add(--l, ans); while (r < qq[i].r) add(++r, ans); while (l < qq[i].l) del(l++, ans); while (r > qq[i].r) del(r--, ans); while (t < qq[i].t) upd(i, t++, ans); while (t > qq[i].t) upd(i, --t, ans); get(ans, qq[i].id), res[qq[i].id] = ans; } return res; } }; ``` ## 例题 ### [P1903 [国家集训队] 数颜色 / 维护队列](https://www.luogu.com.cn/problem/P1903) 这题是带修莫队的板子。这里说明一个块长优化,见[这篇讨论](https://lglg.top/802858)。本题应用中可以取块长为 $1.7 n^{0.667}$,实测自己的板子换了块长后快了 2s。 ```cpp int n, q, a[N], cnt[N], l, r; char opt; void add(int x, int& res) { if (++cnt[a[x]] == 1) res++; } void del(int x, int& res) { if (--cnt[a[x]] == 0) res--; } void _main() { cin >> n >> q; for (int i = 1; i <= n; i++) cin >> a[i]; MBlock<int> mblock(1.7 * pow(n, 0.667)); int id = 0; for (int i = 0; i < q; i++) { cin >> opt >> l >> r; if (opt == 'Q') mblock.add_query(l, r, id), id++; else mblock.add_change(l, r); } vector<int> res = mblock.get( #define qq mblock.qq #define qr mblock.qr add, del, [&](int x, int t, int &res) -> void { if (qq[x].l <= qr[t].l && qr[t].l <= qq[x].r) { // 被区间包含 del(qr[t].l, res); // 删除端点原值 if (++cnt[qr[t].v] == 1) res++; // 加入修改值 } swap(a[qr[t].l], qr[t].v); } #undef qq #undef qr ); for (int i : res) cout << i << '\n'; } ``` # 回滚莫队 ## 原理 ### 可行条件 1. 离线 2. $[l,r]$ 的答案可以以较低复杂度转移到 $[l-1,r],[l,r+1]$ 或者 $[l+1,r],[l,r-1]$。 ### 步骤 很多时候 `add` 和 `del` 中有一个无法顺利进行。此时使用回滚莫队。下面先介绍不删除莫队。 仍然分块并将询问排序。按顺序处理第 $i$ 个询问: - 若左端点所属块 $B_{l_i} \ne B_{l_{i-1}}$,此时将左右端点初始化为 $l'=B_{l_i}+1,r'=B_{l_i}$。 - 若 $B_{l_i}=B_{r_i}$,暴力扫描区间即可,复杂度 $O(\sqrt{n})$。 - 反之,不断 `add` 直到 $l_i=l',r_i=r'$,回答询问,随后撤销左端点的 `add`,即为回滚。 复杂度证明类似,考虑 $r'$ 单调递增,$l'$ 的移动 $\le \sqrt{n}$,认为 $n,q$ 同阶复杂度为 $O(n\sqrt{n})$。 ## 例题 被吃了。 # 二维莫队 被吃了。