【暴力美学!!!】莫队初步
同步发表在了我的公众号。
0. 序章:为什么是莫队?
0.0 一些废话
记得半年前,做题的时候实在是不想写过于重的数据结构(如主席树),于是学习了莫队。
近期又复习了一下莫队,感觉莫队就是优雅的暴力美学。

注意,此篇文章对你的算法基础有较高的要求。
0.1 与常见数据结构的“分工”
面对静态数组的离线区间查询,若被维护的答案能在指针左右扩/缩一格时通过可加可减地更新,莫队就很强:它以“多查询、少变动”为假设,把“全局复杂区间问题”化为“相邻状态的小步子”。
这与“在线 + 可更新”的线段树/树状数组的思路不同:后者依赖结构化信息(如区间合并、幂等等)快速回答每个区间;前者则用排序减少区间端点的大跳跃。作为参照,线段树适合在线可改且能在节点上做可结合运算(如和/最值,或借助懒标记),而 RMQ 等幂等问题甚至用稀疏表一次性预处理后 O(1) 查询即可,根本不需要莫队。
0.2 离线 + 平方分块的直觉
莫队的核心做法是:把所有查询离线后,按左端点所属块再配合右端点排序,让滑动窗口 \([L,R]\) 在相邻查询间移动得尽可能短。块长通常取 \(\lfloor\sqrt{N}\rfloor\),于是平均每次查询只需 \(O(\sqrt N)\) 级别的指针移动(每步为一次 add/remove),总复杂度常见写作 \(O\big((N+Q)\sqrt N\big)\)(常数与维护代价相关)。
0.3 典型收益与边界
当 \(Q\) 与 \(N\) 同阶或更大时,莫队往往从朴素 \(O(QN)\) 拉到 \(O((N+Q)\sqrt N)\) 的可接受区间;但如果答案维护代价很大(如“众数”一类需要多层桶/结构支撑),或者问题本身存在幂等/可加合并的强结构,改用稀疏表/线段树/分块算法更合算。
1. 莫队算法基本面
1.1 适用条件
若一个问题满足以下要点,往往适合莫队:
- 查询可离线排序;
- 不含或只含“可离线处理”的少量修改(带修改版本见后面);
- 维护量可在 \([L,R]\) 伸缩一步时常数级更新(
add/remove); - 你能为值域准备合理的计数/桶/状态,并从中 O(1) 或摊还近似 O(1) 地读出答案(
get)。
1.2 标准排序与块长
最常用的排序规则:
- 先按 \(\lfloor L / B \rfloor\)(左端点所在块)升序;
- 同一块内按 \(R\) 升降序交替(或统一升序)。
一般取 \(B \approx \sqrt{N}\)。交替排序能减少跨块切换时的“之字形”回退。也可统一升序以简化实现,代价是常数略大。
进阶替代:把查询点对映射到空间填充曲线(Hilbert/Peano)顺序,可进一步降低常数,尤其当
add/remove很便宜而排序成本主导时。后续会展开代码与结论。
1.3 三件套:add / remove / answer
实现上保持三件套分工清晰:
add(pos): 把a[pos]纳入当前窗口并更新频次/贡献;remove(pos): 把a[pos]移出并撤销贡献;answer(): 读出当前答案。
保持对称性与可逆性是关键(例如“不同数个数”里,频次从 0→1 时答案 +1,1→0 时答案 −1)。
1.4 入门范例:区间不同数个数
问题:给定数组与若干区间 \([l,r]\),求每个区间中不同数的个数。这是莫队的经典练手题。
思路:维护一个值域频次数组 cnt[x] 与当前“活跃值”的计数 distinct。
add(i): 若cnt[a[i]]从 0 变 1,则distinct++;随后cnt[a[i]]++。remove(i): 先cnt[a[i]]--;若其从 1 变 0,则distinct--。answer(): 直接返回distinct。
复杂度:排序 \(O(Q\log Q)\)(或 \(O(Q)\) 取决于实现),指针移动总步数摊还约 \(O(Q\sqrt N)\),整体常见为 \(O((N+Q)\sqrt N)\)。若引入 Hilbert/Peano 排序,可显著降低常数,数据偏大时尤为明显。
1.5 最小实现骨架
#include <bits/stdc++.h>
using namespace std;
struct Query
{
int l, r, id, bl;
};
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; ++i)
cin >> a[i];
// 离散化(若值域大/离散)
vector<int> b(a.begin() + 1, a.end());
sort(b.begin(), b.end());
b.erase(unique(b.begin(), b.end()), b.end());
for (int i = 1; i <= n; ++i)
a[i] = int(lower_bound(b.begin(), b.end(), a[i]) - b.begin());
int q;
cin >> q;
int B = max(1, int(sqrt(n)));
vector<Query> qs(q);
for (int i = 0; i < q; ++i)
{
int l, r;
cin >> l >> r;
qs[i] = {l, r, i, l / B};
}
sort(qs.begin(), qs.end(), [&](const Query &x, const Query &y) {
if (x.bl != y.bl)
return x.bl < y.bl;
if (x.bl & 1)
return x.r > y.r; // 交替次序,减常数的小技巧
return x.r < y.r;
});
vector<int> cnt(b.size() + 1, 0);
vector<int> ans(q);
int curL = 1, curR = 0, distinct = 0;
auto add = [&](int i) {
int &c = cnt[a[i]];
if (c == 0)
++distinct;
++c;
};
auto rem = [&](int i) {
int &c = cnt[a[i]];
--c;
if (c == 0)
--distinct;
};
for (auto qu : qs)
{
int L = qu.l, R = qu.r;
while (curL > L)
add(--curL);
while (curR < R)
add(++curR);
while (curL < L)
rem(curL++);
while (curR > R)
rem(curR--);
ans[qu.id] = distinct;
}
for (int i = 0; i < q; ++i)
cout << ans[i] << '\n';
return 0;
}
这份骨架体现了莫队的三件套与排序规则;把 add/rem 的内容替换为你的问题的增删逻辑即可。
1.6 复杂度与块长的更细推导
- 设块长 \(B\)。把查询按块排序后,跨块时左端点移动主导,在块内则右端点的变化主导。令每块大约覆盖 \(B\) 个起点,块数约 \(N/B\)。当 \(B \approx \sqrt N\) 时,移动步数的主导项约落在 \(Q\sqrt N\) 数量级。
- 若 \(Q\) 明显偏大或偏小,可以把 \(B\) 做小幅调参(如 \(\max(1,\lfloor N/\max(1,\sqrt Q)\rfloor)\) 的经验式),但一般 \(\sqrt N\) 够用。
- 若
add/remove常数很高(如众数需要多层桶),Hilbert/Peano 排序可显著减少“远跳”,降低 cache miss 与移动总步数。
1.7 常见坑与对拍要点(基础版)
- 区间闭包一致性:模板中统一使用 \([L,R]\) 左闭右闭;
add/rem的位置与while的顺序要与之匹配。 - 离散化:若值域很大必须压缩到连续小整数,避免超大桶。
- 多测试组:频次数组与全局状态务必清零;建议写一个朴素 \(O(QN)\) 的小 checker 做对拍。
- 排序比较器:交替排序要写对(块内奇偶分支),防止指针“回摆过度”。
- IO 与常数:大数据集可换更快的读入,或使用 Hilbert/Peano 排序作为替代(下一卷详述)。
1.8 何时不该用莫队(基础版)
- 幂等/可表合并的区间问题(如 RMQ、GCD),稀疏表/线段树一般更优;
- 需要在线强更新且合并结构清晰的场景(如区间加、区间和),线段树/树状数组更简单直接;
- 强相关跨区间但难以“可加可减”维护的函数(例如众数的严格在线维护)——如果代价太大,需评估是否转向其他结构。
2. 块长选取与复杂度的更细推导
2.1 两种常见的复杂度
- 标准写法:将查询按 \(\lfloor L/B\rfloor\) 分块、块内按 \(R\) 排序,整体步数约为 \(\mathcal O\big((N+Q)\sqrt N\big)\)(
add/remove的单位代价计入常数项 \(F\))。 - “块数 K” 视角:把序列分成 \(K\) 个块,可得到 \(\mathcal O\!\left(Q\cdot\frac{N}{K}+K\cdot N\right)\) 的移动上界。选 \(K=\sqrt N\) 时回到 \(\mathcal O\big((N+Q)\sqrt N\big)\)。
小结:无论从哪个视角,默认选 \(B\approx\sqrt N\) 既简洁又可靠。把 \(F\) 控制在常数级,是莫队可行的第一要务。
2.2 何时考虑“按 \(Q\) 调参”的选择
在一些极端任务里(比如 \(Q\ll N\) 或 \(Q\gg N\)),会有人用经验公式把块长调到 \(B\approx\max\!\big(1,\lfloor N/\sqrt{\max(1,Q)}\rfloor\big)\) 来降低常数。它不改变“量级上界”的经典结论,但实践里确实可能更快或更稳。你可以当作一条经验,而不是需要严格背诵的结论。
2.3 什么时候“换排序”能显著提速
如果你的 add/remove 已经非常便宜,而移动指针的 cache 友好性与回拉距离成为瓶颈,那么用空间填充曲线(Hilbert/Peano)排序通常能以更低的常数跑过“分块排序”。
3 实现细节总览(离散化 / 计数桶布局 / (I/O) / 对拍)
3.1 离散化与值域压缩
- 何时需要:当输入值域很大(例如值达到 \(10^9\))或稀疏时,用离散化把它们映射到 \([0..M-1]\) 的连续小整数,令计数桶
cnt[val]可用数组实现,保证增删为 O(1)。 - 做法简述:复制数组、排序去重,再
lower_bound回写原数组。遇到多组数据时,要么复用缓冲并在每组结束后清零,要么用时间戳技巧。
3.2 计数桶布局与缓存友好
-
频次数组:最常见是
cnt[value]与某些“派生桶”(如freqCount[f]统计“出现次数恰为 f 的值的个数”,用于“最大频次/众数系”)。 -
布局建议:
- 将大数组开成全局(或
static),避免反复分配; - 尽量连续访问(减小跨页跨度),减少 cache miss。
- 将大数组开成全局(或
-
曲线排序与 locality:当 add/remove 很便宜、而指针“折返”开销占主导时,采用 Hilbert/Peano 排序可显著改善访问局部性,常被用来压常数。
3.3 快速 I/O 与分支最小化
- 大数据下建议使用
ios::sync_with_stdio(false); cin.tie(nullptr);; add/remove内减少分支:例如将“计数从 0→1 时答案 +1;1→0 时 −1”的分支写在最外侧,内部只做自增自减;- 块长调参:虽然“\(\sqrt N\)”是可靠默认,但在极端 \(Q\) 与 \(N\) 比例下,工程上有人按 \(B\approx N/\sqrt{\max(1,Q)}\) 微调,实践中能小降常数。
4 入门范例一:区间不同数个数
这是莫队最经典的问题。核心是维护 cnt[val] 与 distinct。
4.1 思路回顾
add(i):若cnt[a[i]]由 0 变 1,则distinct++,随后cnt++;remove(i):先cnt--,若由 1 变 0,则distinct--;answer():返回distinct。
4.2 参考实现
#include <bits/stdc++.h>
using namespace std;
struct Query
{
int l, r, id, bl;
};
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i];
// 离散化
vector<int> b(a.begin() + 1, a.end());
sort(b.begin(), b.end());
b.erase(unique(b.begin(), b.end()), b.end());
for (int i = 1; i <= n; i++)
a[i] = int(lower_bound(b.begin(), b.end(), a[i]) - b.begin());
int q;
cin >> q;
int B = max(1, int(sqrt(n)));
vector<Query> qs(q);
for (int i = 0; i < q; i++)
{
int l, r;
cin >> l >> r;
qs[i] = {l, r, i, l / B};
}
sort(qs.begin(), qs.end(), [&](const Query &x, const Query &y) {
if (x.bl != y.bl)
return x.bl < y.bl;
return (x.bl & 1) ? x.r > y.r : x.r < y.r; // 交替
});
vector<int> cnt(b.size() + 1);
cnt.assign(b.size() + 1, 0);
vector<int> ans(q);
int curL = 1, curR = 0, distinct = 0;
auto add = [&](int i) {
int &c = cnt[a[i]];
if (c == 0)
++distinct;
++c;
};
auto rem = [&](int i) {
int &c = cnt[a[i]];
--c;
if (c == 0)
--distinct;
};
for (auto qu : qs)
{
int L = qu.l, R = qu.r;
while (curL > L)
add(--curL);
while (curR < R)
add(++curR);
while (curL < L)
rem(curL++);
while (curR > R)
rem(curR--);
ans[qu.id] = distinct;
}
for (int i = 0; i < q; i++)
cout << ans[i] << "\n";
return 0;
}
5 入门范例二:区间“最大频次 / 众数系”
5.1 难点与典型权衡
-
“最大频次”(\(\max_f\))相对“众数本身”要容易:只需要知道当前窗口内“最大的出现次数是多少”。
-
思路:维护两层结构
cnt[val]:每个值的出现次数;bucket[f]:出现次数恰为 \(f\) 的值的个数。
窗口变化时:某个值的频次从 \(f\to f+1\) 或 \(f\to f-1\),相应更新bucket;记录curMaxF并在需要时向下修正(当bucket[curMaxF]==0时递减)。
-
复杂度:
add/remove仍是 O(1),求最大频次只在个别时刻递减curMaxF,总体摊还是 O(1)。若要求众数的值,通常还需在各频次里维持候选(成本更高、实现更重),很多人会改为“求最大频次”或“出现次数 ≥ K 的计数”等替代表述。
5.2 参考实现(返回“最大频次”)
#include <bits/stdc++.h>
using namespace std;
struct Query
{
int l, r, id, bl;
};
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i];
// 离散化
vector<int> b(a.begin() + 1, a.end());
sort(b.begin(), b.end());
b.erase(unique(b.begin(), b.end()), b.end());
for (int i = 1; i <= n; i++)
a[i] = int(lower_bound(b.begin(), b.end(), a[i]) - b.begin());
int q;
cin >> q;
int B = max(1, int(sqrt(n)));
vector<Query> qs(q);
for (int i = 0; i < q; i++)
{
int l, r;
cin >> l >> r;
qs[i] = {l, r, i, l / B};
}
sort(qs.begin(), qs.end(), [&](const Query &x, const Query &y) {
if (x.bl != y.bl)
return x.bl < y.bl;
return (x.bl & 1) ? x.r > y.r : x.r < y.r;
});
int M = b.size();
vector<int> cnt(M + 1, 0);
// 最大频次理论上不超过区间长度,保守可开到 n
vector<int> bucket(n + 1, 0);
vector<int> ans(q);
int curL = 1, curR = 0, curMaxF = 0;
auto add = [&](int i) {
int v = a[i], f = cnt[v];
if (f > 0)
--bucket[f];
++cnt[v];
++bucket[f + 1];
if (f + 1 > curMaxF)
curMaxF = f + 1;
};
auto rem = [&](int i) {
int v = a[i], f = cnt[v];
--bucket[f];
--cnt[v];
if (f - 1 > 0)
++bucket[f - 1];
while (curMaxF > 0 && bucket[curMaxF] == 0)
--curMaxF;
};
for (auto qu : qs)
{
int L = qu.l, R = qu.r;
while (curL > L)
add(--curL);
while (curR < R)
add(++curR);
while (curL < L)
rem(curL++);
while (curR > R)
rem(curR--);
ans[qu.id] = curMaxF;
}
for (int i = 0; i < q; i++)
cout << ans[i] << "\n";
return 0;
}
若题目需要“\(\ge K\) 的频次的元素个数”,只需把
bucket[f]的前缀和(或分块维护)变成查询对象即可。
5.3 何时应放弃“精确众数值”
精确众数(返回某个出现次数最高的值)在莫队下往往需要更重的结构,实现复杂度与常数都很高;若数据范围允许,很多选手会转向线段树+分治。
6 入门范例三:区间 MEX(最小缺失值)
6.1 维护结构
- 仍用
cnt[val]统计出现次数; - 维护一个块分解的“零计数桶”:把值域分成若干块,记录每块中“
cnt==0的位置个数”; - “求 MEX”时:先找到首个“零个数>0”的块,再在线性扫描该块内找到第一个
cnt==0的位置。这样能在 O(值域块数 + 块长) 的复杂度内得到 MEX。
6.2 参考实现(返回当前窗口 MEX)
#include <bits/stdc++.h>
using namespace std;
struct Query
{
int l, r, id, bl;
};
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i];
int q;
cin >> q;
int B = max(1, int(sqrt(n)));
vector<Query> qs(q);
for (int i = 0; i < q; i++)
{
int l, r;
cin >> l >> r;
if (l > r)
swap(l, r); // 若题面不保证,可保底
qs[i] = {l, r, i, l / B};
}
sort(qs.begin(), qs.end(), [&](const Query &x, const Query &y) {
if (x.bl != y.bl)
return x.bl < y.bl;
return (x.bl & 1) ? x.r > y.r : x.r < y.r;
});
// 关键点:不做离散化!只维护 0..V-1 的计数
int V = n + 2; // MEX ≤ n,留一点余量
int S = max(1, int(sqrt(V)));
int NB = (V + S - 1) / S;
vector<int> cnt(V, 0), zero(NB, 0);
for (int i = 0; i < V; i++)
zero[i / S]++; // 初始全为“没出现”
auto inc = [&](int v) {
if (0 <= v && v < V)
{
if (cnt[v] == 0)
zero[v / S]--;
++cnt[v];
}
};
auto dec = [&](int v) {
if (0 <= v && v < V)
{
--cnt[v];
if (cnt[v] == 0)
zero[v / S]++;
}
};
auto get_mex = [&]() {
int blk = 0;
while (blk < NB && zero[blk] == 0)
++blk;
if (blk == NB)
return V; // 理论上到不了
int L = blk * S, R = min(V - 1, (blk + 1) * S - 1);
for (int x = L; x <= R; x++)
if (cnt[x] == 0)
return x;
return V;
};
vector<int> ans(q);
int curL = 1, curR = 0;
auto add = [&](int i) { inc(a[i]); };
auto rem = [&](int i) { dec(a[i]); };
for (auto qu : qs)
{
int L = qu.l, R = qu.r;
while (curL > L)
add(--curL);
while (curR < R)
add(++curR);
while (curL < L)
rem(curL++);
while (curR > R)
rem(curR--);
ans[qu.id] = get_mex();
}
for (int i = 0; i < q; i++)
cout << ans[i] << "\n";
return 0;
}
6.3 复杂度与实战提示
- 单次
get_mex()的成本约为 \(O(\text{块数} + \text{块长})\approx O(\sqrt{V})\),在莫队的总框架下仍然可接受; - 若值域极大(比如原值域 \(10^9\)),离散化(确保
V比“窗口里可能出现的最大 MEX”略大)是关键,否则 MEX 可能“撞上边界”;
7. 扩展阅读:空间填充曲线排序(Hilbert / Peano)
7.1 为什么它们能“更快”
Hilbert / Peano 都把二维点 \((L,R)\) 映射为一条连续穿过所有格点的曲线序,使得相邻查询在二维平面上彼此接近,从而减少指针大幅回摆。理论量级仍是莫队的那一档,但常数往往更小。
7.2 Hilbert 排序:实现要点与模板
要点
- 选择 \(P\) 使 \(2^P\) 覆盖到 \(\max(L,R)\) 的范围(例如 \(P=21 \sim 22\) 足以覆盖 \(2\times10^6\) 级别坐标)。
- 为每个查询计算
ord = hilbertOrder(L, R, P, 0),再按ord从小到大排序即可。
参考实现(可直接替换比较器)
下面给出一个常用且稳定的 Hilbert 编码函数与最小骨架。你只需把上一卷的 Query 加个 ord 字段,或在排序时临时计算并比较即可。
#include <bits/stdc++.h>
using namespace std;
// 计算 (x,y) 在 2^pow × 2^pow 网格上的 Hilbert 次序
static inline long long hilbertOrder(int x, int y, int pow, int rot)
{
if (pow == 0)
return 0;
int h = 1 << (pow - 1);
int seg = (x < h) ? ((y < h) ? 0 : 3) : ((y < h) ? 1 : 2);
seg = (seg + rot) & 3;
static const int nxt[4] = {3, 0, 0, 1};
int nx = x & (h - 1), ny = y & (h - 1);
int nrot = (rot + nxt[seg]) & 3;
long long add = hilbertOrder(nx, ny, pow - 1, nrot);
long long base = 1LL << (2 * pow - 2);
if (seg == 1 || seg == 2)
return 1LL * seg * base + add;
else
return 1LL * seg * base + (base - add - 1);
}
struct Query
{
int l, r, id;
long long ord;
};
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; ++i)
cin >> a[i];
int q;
cin >> q;
vector<Query> qs(q);
for (int i = 0; i < q; ++i)
{
int L, R;
cin >> L >> R;
qs[i] = {L, R, i, 0};
}
int P = 0;
while ((1 << P) <= max(n, 1))
++P; // 估一个 pow
for (auto &x : qs)
x.ord = hilbertOrder(x.l, x.r, max(P, 1), 0);
sort(qs.begin(), qs.end(), [&](const Query &A, const Query &B) { return A.ord < B.ord; });
vector<int> cnt(1 << 20, 0); // 按需换成离散化后的大小
vector<int> ans(q);
int curL = 1, curR = 0, cur = 0;
auto add = [&](int i) {
int &c = cnt[a[i]];
if (c == 0)
++cur;
++c;
};
auto rem = [&](int i) {
int &c = cnt[a[i]];
--c;
if (c == 0)
--cur;
};
for (auto qu : qs)
{
int L = qu.l, R = qu.r;
while (curL > L)
add(--curL);
while (curR < R)
add(++curR);
while (curL < L)
rem(curL++);
while (curR > R)
rem(curR--);
ans[qu.id] = cur;
}
for (int i = 0; i < q; ++i)
cout << ans[i] << '\n';
return 0;
}
7.3 Peano 排序:另一条曲线的思路
Peano 是把平面划成 \(3^k\times 3^k\) 的九宫格递归,访问顺序呈“Z-蛇形”与旋转组合。相较 Hilbert,它在某些实现里可获得更规整的分块递归,常数也相当可观。若你有兴趣做进一步微调或三维扩展(带修改时把“时间”也纳入曲线),Peano 是值得一试的替代项。
何时选 Peano?
- 当你的平台/数据分布下,Hilbert 的旋转规则带来的分支开销稍重,或你准备做三维曲线(把 \(T\) 一起映射)时,Peano会有不错的工程手感。
7.4 与“按块排序”的取舍
- 优点:空间曲线更“平滑”,相邻查询平均改动更小,cache 友好;代码里不再出现块奇偶交替的分支。
- 缺点/注意:编码函数的递归/位运算有少量开销;若查询本就稀疏且 \(Q\) 不大,收益可能不明显。极端题目中,还要注意你的桶/结构是否因为“访问轨迹”而膨胀(有讨论指出在特定题目里 Hilbert 写法出现了 MLE,需要你回到离散化与桶布局本身找原因,而不是曲线本身的锅)。
8. 带修改莫队(三维 / 时间维 T)
8.1 模型
当查询与“点修改”穿插出现时,我们把时间也当成一维:
- 对每条查询 \(q_i=(L_i,R_i,t_i)\),其中 \(t_i\) 表示这条查询之前已经发生的修改次数;
- 维护三指针 \((L,R,T)\) 并提供以下操作:
add_left / add_right / remove_left / remove_right和apply_update / rollback_update,使得我们可以在移动到下一条查询时,把窗口与时间都调整到目标状态。
8.2 排序与复杂度直觉
- 经典排序做法:对 \((L,R,T)\) 使用分块排序(例如 \(L\) 按块、块内按 \(R\)、再按 \(T\)),使相邻查询的总体位移尽量小;
- 摊还复杂度仍可写成 \(O\left((N+Q)\sqrt N\right)\) 级别乘上一个与“时间维切换次数”相关的常数,具体要看你的
apply/rollback代价;
8.3 维护策略:两套增删 + 改动“命中窗口”的判定
设修改集为 \(\{(p_k, \text{old}_k, \text{new}_k)\}\)。当 \(T\) 前移(应用第 \(T+1\) 次修改)或后退(回滚第 \(T\) 次修改)时:
- 如果修改位置 \(p_k\) 在当前 \([L,R]\) 中,就要把旧值从计数结构里“remove”,再把新值“add”(或回滚时相反);
- 若不在窗口内,只需更新数组本身,不触碰计数结构。
这保证“时间维移动”的代价与普通add/remove同阶,是带修改莫队能跑起来的关键。
8.4 实战细节与坑
- 离散化 依旧重要:值域大时先压缩到 \([0..M)\),便于 O(1) 桶;
- 排序块长 可按经验用 \(B \approx N^{2/3}\)(或把 \(L\) 与 \(R\) 用同一块长、\(T\) 用单独块长)以平衡三维移动的常数。
- 缓存友好:若
apply/rollback很频繁,考虑把修改按时间块聚类或尝试三维曲线排序(Peano/Hilbert);
8.5 可抄骨架
示意骨架强调流程;你只需把
add/remove/apply/rollback/get改成你的题意即可。
#include <bits/stdc++.h>
using namespace std;
// 离散化工具
static inline void compress(vector<int> &a)
{
vector<int> b = a;
sort(b.begin(), b.end());
b.erase(unique(b.begin(), b.end()), b.end());
for (auto &x : a)
x = int(lower_bound(b.begin(), b.end(), x) - b.begin());
}
struct Q
{
int l, r, t, id;
}; // 查询:带时间戳 t(之前的修改数)
struct U
{
int p, prev, nxt;
}; // 修改:位置、旧值、新值
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i];
int m;
cin >> m;
vector<Q> qs;
qs.reserve(m);
vector<U> us;
us.reserve(m);
vector<tuple<int, int, int>> raw; // 读入操作:type, x, y
for (int i = 0; i < m; i++)
{
int tp, x, y;
cin >> tp >> x >> y;
raw.emplace_back(tp, x, y);
}
// 为了离散化,把出现过的值先收集:初值 + 所有修改的新值
vector<int> pool;
pool.reserve(n + m + 5);
for (int i = 1; i <= n; i++)
pool.push_back(a[i]);
for (auto [tp, x, y] : raw)
if (tp == 2)
pool.push_back(y);
sort(pool.begin(), pool.end());
pool.erase(unique(pool.begin(), pool.end()), pool.end());
auto enc = [&](int v) { return int(lower_bound(pool.begin(), pool.end(), v) - pool.begin()); };
for (int i = 1; i <= n; i++)
a[i] = enc(a[i]);
int T = 0; // 已应用修改数
for (int i = 0; i < m; i++)
{
int tp, x, y;
tie(tp, x, y) = raw[i];
if (tp == 1)
{ // query
qs.push_back({x, y, T, (int)qs.size()});
}
else
{ // update
int p = x, nv = enc(y);
us.push_back({p, a[p], nv}); // 记录修改时的“旧值”
a[p] = nv; // 把数组推进到最新,稍后执行离线回放
T++;
}
}
// 还原 a 到最初状态,准备离线回放
for (int i = (int)us.size() - 1; i >= 0; i--)
a[us[i].p] = us[i].prev;
// 选择块长(经验值:三维时可用 n^(2/3) 级别)
int nblk = max(1, (int)pow(n, 2.0 / 3.0));
sort(qs.begin(), qs.end(), [&](const Q &A, const Q &B) {
int b1 = A.l / nblk, b2 = B.l / nblk;
if (b1 != b2)
return b1 < b2;
int b3 = A.r / nblk, b4 = B.r / nblk;
if (b3 != b4)
return b3 < b4;
return A.t < B.t;
});
// 计数结构(示例:维护“不同数个数”)
int V = (int)pool.size();
vector<int> cnt(V, 0);
int cur = 0;
auto add = [&](int pos, vector<int> &arr) {
int &c = cnt[arr[pos]];
if (c == 0)
++cur;
++c;
};
auto rem = [&](int pos, vector<int> &arr) {
int &c = cnt[arr[pos]];
--c;
if (c == 0)
--cur;
};
// 时间维应用/回滚
auto apply = [&](int k, int L, int R, vector<int> &arr) {
int p = us[k].p, nv = us[k].nxt, ov = arr[p];
if (L <= p && p <= R)
{
// 先把旧值移出,再放新值
--cnt[ov];
if (cnt[ov] == 0)
--cur;
if (cnt[nv] == 0)
++cur;
++cnt[nv];
}
arr[p] = nv;
};
auto rollb = [&](int k, int L, int R, vector<int> &arr) {
int p = us[k].p, ov = us[k].prev, nv = arr[p];
if (L <= p && p <= R)
{
--cnt[nv];
if (cnt[nv] == 0)
--cur;
if (cnt[ov] == 0)
++cur;
++cnt[ov];
}
arr[p] = ov;
};
vector<int> ans(qs.size());
int L = 1, R = 0, t = 0; // 当前区间与时间
// 把 a 作为“活动数组”,随时间维变更
vector<int> arr = a;
for (auto q : qs)
{
while (t < q.t)
apply(t++, L, R, arr);
while (t > q.t)
rollb(--t, L, R, arr);
while (L > q.l)
add(--L, arr);
while (R < q.r)
add(++R, arr);
while (L < q.l)
rem(L++, arr);
while (R > q.r)
rem(R--, arr);
ans[q.id] = cur; // 把“当前答案”改成你的问题的 get()
}
for (int x : ans)
cout << x << "\n";
return 0;
}
提示
- 上面骨架用“不同数个数”示范,替换
cur/cnt逻辑即可实现“最大频次”“阈值计数”等可加可减目标;
9. 树上莫队(路径查询)
9.1 目标与套路总览
树上路径 \((u,v)\) 的查询可通过 Euler Tour + LCA 化到序列问题,然后在序列上做莫队。关键点:
- 对树做 DFS 得到
dfn[u]与“进入/离开”的欧拉序列; - 每个节点出现两次(进与出),配合“存在性翻转”技巧:当指针经过某个结点的任一出现位置时,就“翻转”该节点在当前集合中的存在状态;
- 对于路径 \((u,v)\),若
lca(u,v)=w,则在区间答案上补偿 LCA \(w\) 一次(因为欧拉映射带来“端点计数少一次”的现象)。
9.2 Euler Tour 与 LCA 预处理
- LCA 可用 倍增 或 Euler Tour + RMQ;
- Euler Tour 具体做法:DFS 记录访问序列与深度,或只记录首次出现
dfn[u]与节点的进出时间tin/tout,供判定祖先关系使用。
9.3 查询映射
常用两种映射方式:
-
成对出现 + 翻转法(最流行):构造长度 \(2N\) 的欧拉序列
euler[1..2N],每个节点出现两次;把每条路径 \((u,v)\) 转成对欧拉序列上的一个区间 \([L,R]\),指针移动时对访问到的欧拉位置 \(pos\) 的节点x执行toggle(x):- 若
x当前不在集合中,则add(x);否则remove(x); - 当区间覆盖完整的路径时,集合中的节点刚好是路径上的“每个节点的奇偶出现”之和为 1 的那些,再对 LCA 做一次补偿(若 LCA 不在集合或需要权值计入)。
- 若
-
基于 dfs 序的单出现映射:维护每个节点的首次出现
dfn[u],把路径拆解为两段并搭配 LCA 修正(实现细节略多,本文不展开)。
9.4 可加可减目标与典型维护
- 不同数个数 / 频次类:对节点权值做离散化与计数桶;
- 阈值问题(如“≥K 次的值有多少个”)可沿用数组上莫队的“双层桶”技巧;
- 边权/边信息:建双倍节点或在欧拉序上记“进入边”,即可把边的属性转成在节点“进入时添加”的属性。
9.5 可抄骨架
省略若干细节。把
add/remove/toggle改成你的题意即可。
#include <bits/stdc++.h>
using namespace std;
struct Q
{
int l, r, id, u, v, w;
};
const int MAXN = 200000 + 5;
int n, q;
vector<int> g[MAXN];
int val[MAXN];
int up[20][MAXN], dep[MAXN], tin[MAXN], tout[MAXN], timer_ = 0;
int eul[2 * MAXN];
int firstpos[MAXN];
void dfs(int u, int p)
{
up[0][u] = p;
dep[u] = (p == 0 ? 0 : dep[p] + 1);
tin[u] = ++timer_;
eul[timer_] = u;
for (int k = 1; k < 20; k++)
up[k][u] = up[k - 1][u] ? up[k - 1][up[k - 1][u]] : 0;
for (int v : g[u])
if (v != p)
{
dfs(v, u);
}
tout[u] = ++timer_;
eul[timer_] = u;
}
int lca(int a, int b)
{
if (dep[a] < dep[b])
swap(a, b);
int d = dep[a] - dep[b];
for (int k = 19; k >= 0; k--)
if (d >> k & 1)
a = up[k][a];
if (a == b)
return a;
for (int k = 19; k >= 0; k--)
if (up[k][a] != up[k][b])
{
a = up[k][a];
b = up[k][b];
}
return up[0][a];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> val[i];
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
vector<int> pool(n);
for (int i = 1; i <= n; i++)
pool[i - 1] = val[i];
sort(pool.begin(), pool.end());
pool.erase(unique(pool.begin(), pool.end()), pool.end());
for (int i = 1; i <= n; i++)
val[i] = int(lower_bound(pool.begin(), pool.end(), val[i]) - pool.begin());
vector<Q> qs;
qs.reserve(q);
for (int i = 0; i < q; i++)
{
int u, v;
cin >> u >> v;
if (tin[u] > tin[v])
swap(u, v);
int w = lca(u, v);
int L, R;
if (w == u)
{
L = tin[u];
R = tin[v];
}
else
{
L = tout[u];
R = tin[v];
}
qs.push_back({L, R, i, u, v, w});
}
int B = max(1, int(sqrt(2 * n)));
sort(qs.begin(), qs.end(), [&](const Q &A, const Q &BQ) {
int blA = A.l / B, blB = BQ.l / B;
if (blA != blB)
return blA < blB;
return (blA & 1) ? A.r > BQ.r : A.r < BQ.r;
});
vector<int> cnt(pool.size() + 1, 0), in(n + 1, 0), ans(q);
int cur = 0;
auto addnode = [&](int x) {
int &c = cnt[val[x]];
if (c == 0)
++cur;
++c;
};
auto remnode = [&](int x) {
int &c = cnt[val[x]];
--c;
if (c == 0)
--cur;
};
auto toggle = [&](int pos) {
int x = eul[pos];
if (in[x])
{
remnode(x);
in[x] = 0;
}
else
{
addnode(x);
in[x] = 1;
}
};
int L = 1, R = 0;
for (auto qu : qs)
{
while (L > qu.l)
toggle(--L);
while (R < qu.r)
toggle(++R);
while (L < qu.l)
toggle(L++);
while (R > qu.r)
toggle(R--);
int w = qu.w;
if (!in[w])
addnode(w);
ans[qu.id] = cur;
if (!in[w])
remnode(w);
}
for (int i = 0; i < q; i++)
cout << ans[i] << "\n";
return 0;
}
10. 与平方分块 / 稀疏表 / 分治的边界与取舍
一句话版总览
- 问题“可离线 + 可加可减”→ 往莫队靠;
- 幂等可合并(min/max/gcd/RMQ)→ 稀疏表一招制胜;
- 在线强更新、结构化合并(区间加/和/最值)→ 线段树/树状数组/分块;
- 树上:路径类倾向“树上莫队 + LCA”,子树/整棵并集统计倾向“DSU on tree(small-to-large)”。
10.1 莫队 vs. 平方分块(Sqrt Decomposition)
两者都源自“按块组织工作量”,但切入点不同:
- 莫队:把查询离线排序,尽量缩短相邻查询间 \([L,R]\) 的指针移动,靠
add/remove的常数级更新取胜; - 平方分块:把数据分块,块内与块间分别维护可合并信息(如块内和、块内最大值),查询在“整块 + 两个散块”中合并答案,更新在“块内”处理。
当你需要在线地(不可离线)回答许多“区间和/最值/计数”且更新操作不复杂时,分块/树状数组/线段树会比莫队自然;当所有查询可离线、且add/remove足够轻时,莫队通常常数更稳。
10.2 幂等可合并问题:稀疏表 > 莫队
如果目标运算是幂等且可合并(如 RMQ 的 min/max、gcd),直接用稀疏表:预处理 \(O(n\log n)\),查询 \(O(1)\),而且实现极短;这类问题使用莫队往往画蛇添足。
10.3 树上问题:树上莫队 vs. DSU on tree
- 树上莫队(路径类):把路径 \((u,v)\) 通过 Euler Tour + LCA 映射到序列区间,配合“翻转存在性”维护路径上的点/权统计;适合路径上不同数/频率函数等“可加可减”的答案。
- DSU on tree / small-to-large(子树类):把每个子树的一堆信息“小并大”累加,摊还 \(O(n\log n)\) 甚至近线性;适合子树并集这类天然“增而不减”的统计(不同色数、出现次数≥K等)。
经验分界:路径就用树上莫队,子树就用 DSU on tree(非绝对)。如果问题两者都能做,谁的
add/remove/合并常数更低,就选谁。
10.4 分治/离线技巧何时更优
像“区间逆序对/小于等于 K 的对数/二维偏序”之类,本质是可排序维度多、答案可拆为前缀贡献的题,常选 CDQ/归并分治 + 树状数组,而不是莫队。
它们利用“按一个维度排序,另一个维度用数据结构”的套路自然离线,少指针折返、更 Cache-friendly。莫队当然也能硬上,但往往常数更大、实现更绕。
10.5 空间填充曲线排序与卡常地带
当你确认莫队是正解但常数吃紧(尤其 add/remove 很便宜时),用 Hilbert 或 Peano 排序通常显著提速。
11. 可维护函数族与“依赖频次向量”的问题
核心判据
把当前窗口的内容抽象成频次向量 \(c_x=\#\{a_i=x\}\)。若答案 \(F\) 能写成对 \(\{c_x\}\) 的可逆增删(把某个 \(c_x\) 改为 \(c_x\pm1\) 时能 \(O(1)\) 更新 \(F\)),就天然适合莫队。
11.1 经典“依赖频次”的可维护目标
- 不同数个数:维护
cnt[x]与“活跃值计数”;0→1加一,1→0减一。 - 等值对/三元组计数:维护 \(\sum_x \binom{c_x}{2}\) 或 \(\sum_x \binom{c_x}{3}\) 即可;增删时只改动涉及的单个 \(x\) 的项。
- 最大频次 / 至少 K 次的值的个数:双层桶
bucket[f]=#{x: c_x=f},再维护curMaxF或“\(\ge K\)”的计数。 - MEX:本质是找最小的 \(x\) 满足 \(c_x=0\)。用“值域分块的零计数桶”或位集/树结构即可摊还 \(O(\sqrt V)\) 或 \(O(\log V)\) 维护。
- “前缀哈希/前缀异或”派生类:典型如 CF 617E “XOR and Favorite Number”。把原数组转为前缀异或 \(p[i]\),区间询问化为“统计区间内有多少对 \((i,j)\) 使 \(p[i]\oplus p[j]=K\)”;维护“前缀值频次”即可用莫队在 \(O(1)\) 增删中更新答案。
11.2 需要小心或不推荐的目标
- 严格“众数的值”:在莫队下维护“最大频次对应的某个值”通常要更重的结构(多层桶/有序结构),代码与常数显著上升;若赛题允许,常改为“最大频次/≥K 的个数/是否≥K”。
- 非可逆、强顺序依赖:例如“把答案写成区间有序统计第 k 小且频繁变化”的需求,
add/remove往往需要平衡树维护,常数、实现复杂度双高;若题目允许在线结构,更宜直接线段树/主席树。 - 跨元素强相关的复杂代价式:一旦
add/remove需要“全局重算”或扫描多个桶,莫队意义不大。
11.3 快速检查清单(给出就能上莫队)
- 离线可行吗?
- 你能把答案写成 \(\{c_x\}\) 上的函数吗?
- 当某个 \(c_x\) 改为 \(c_x\pm1\) 时,是否能 \(O(1)\) 更新答案?
- 值域是否可离散化到可承受的桶大小?
- 若
add/remove很快但 TLE,是否考虑 Hilbert/Peano 排序降常?
12. 常见坑与 Debug 清单
12.1 区间闭包与指针顺序
- 全文统一用左闭右闭
[L, R]。一旦写成半开区间或把add/remove的顺序写反,答案会在端点处“抖动”。建议把四条指针移动写成固定模板:
while (curL > L)
add(--curL);
while (curR < R)
add(++curR);
while (curL < L)
rem(curL++);
while (curR > R)
rem(curR--);
并在小数据上做对拍。
12.2 值域离散化与桶大小
- 值域过大时必须压缩为连续小整数,否则
cnt[]会炸内存或 TLE。把“初值 + 所有可能写入的新值(带修改题)”一起收集再离散化,避免修改时落到“未分配”区间。
12.3 计数溢出与类型选择
- 诸如 CF 617E“统计等值对/异或对”的题,答案可能达 \(O(n^2)\)。
ans、中间乘法、组合数累加都要用 64 位(long long)或更大。
12.4 排序比较器与稳定性
- 经典块排序:按
L/B分块,块内R交替升降以减小“回拉”。比较器写错(比如奇偶判断反了)会导致频繁“折返”。若改用 Hilbert/Peano,给每个查询计算一个 64 位序值ord后直接按ord排即可。
12.5 多测试组与清零
- 评测常有多组数据,
cnt[]、in[](树上莫队的“是否在集合”标记)、答案累加器都要在每组结束后完整复位。若值域很大,考虑“时间戳数组”替代全清零(以vis[val] == stamp判断是否“被使用过”)。
12.6 树上莫队的 LCA 补偿
- 路径 \((u,v)\) 做成欧拉序区间后,集合中节点是“出现奇偶为 \(1\) 的那些”,LCA 需单独补一次(如果题意需要计入 LCA 的权)。忘记补偿或重复补偿都会 WA。
12.7 带修改(三维)中的时间维操作
apply_update/rollback_update只有在修改位置落在当前[L,R]内时,才对计数结构做“先移出旧值、再加入新值”(回滚反之);否则只更新底层数组。把这点写反会出现“幽灵贡献”。
12.8 “玄学 RE/MLE”与内存布局
- 遇到这种案例,优先自查越界、初始化、下标与闭包,再考虑更换编译参数或局部调整内存布局。
12.9 Hilbert/Peano 的参数与溢出
- Hilbert 的
pow要覆盖坐标最大值(如 \(2^{21}\) 能盖到约两百万的坐标),序值建议用 64 位;Peano 的 3 叉递归也要避免中间溢出。基准与教程会给出安全的模板与参数范围。
12.10 树与欧拉序的前置
- 树上莫队前需要可靠的 LCA(倍增或欧拉+RMQ)。把
tin/tout、euler[]写乱会直接影响“翻转存在性”正确性。
13. 毒瘤卡常:让莫队飞起来!
警告:本部分为拓展阅读,对莫队算法还不是很熟悉的同学请尽量跳过,以防误导。
13.1 排序:从块排序切到空间曲线
- 在
add/remove已经 O(1) 的前提下,排序序列的“局部性”显著影响常数。把[L,R]映射到 Hilbert 序通常能明显提速;Peano 作为另一条空间曲线,在一些实现里也有良好常数。
13.2 三维(带修改)时的块长选择
- 三维莫队常用经验:把
L、R、T都分块或让T用更大的块;不少实现用 \(B\approx n^{2/3}\) 平衡三维移动。也有人采用“(L 块, R 块, T)”三层键按字典序。
13.3 计数结构的局部性与层次化桶
- “最大频次/≥K 次个数”之类用双层桶:
cnt[val]+bucket[freq],并缓存当前maxFreq或“≥K 的总数”。这种设计把每次更新压到常数,避免全局扫描。
13.4 分支优化
add/remove内尽量减少分支、合并相邻自增自减;- 若你用块排序,预先把每个位置的块号存好,比较器里只读整型字段,避免反复
i/B。
13.5 树上莫队:翻转快路径
- 把
toggle(pos)写成“命中节点 x:若in[x]为 1 就移除,否则加入”,并让addnode/remnode极简;权值离散化后cnt[val[x]]连续存放、命中率更高。
13.6 题型边界上的更优替代
- 若运算幂等且可合并(min/max/gcd/RMQ),优先上 稀疏表:预处理 \(O(n\log n)\)、查询 \(O(1)\),代码短常数小;
- 若需要在线强更新(区间加/和/最值),分块/线段树/树状数组通常更适合;
- 子树并集类在树上常用 DSU on tree。
14. FAQ
Q1:我的区间问题到底适不适合用莫队?
A:三步快检:① 查询可离线;② 答案能写成“只依赖频次数组”的函数;③ add/remove 在单点进出时可 \(O(1)\) 或摊还 \(O(1)\) 更新。三步都过,基本就合适。若是 RMQ/GCD 这类幂等可合并问题,优先稀疏表;若需要大量在线更新(区间加/和/最值),优先树状数组/线段树/分块。
Q2:块长一定取 \(\sqrt{N}\) 吗?
A:默认取 \(\lfloor\sqrt N\rfloor\) 就很稳;在 \(Q\) 极大或极小时,工程上有人用 \(B\approx N/\sqrt{\max(1,Q)}\) 做微调,常数会更舒服,但量级不变。带修改(三维)常用 \(B\sim N^{2/3}\) 一类的均衡选择。
Q3:为什么很多人把排序从“分块排序”换成 Hilbert/Peano?
A:为降低常数。空间填充曲线能让相邻查询在 \((L,R)\) 平面上更“邻近”,减少指针大回摆与 cache miss;当 add/remove 很便宜时,曲线排序往往显著提速。
Q4:带修改的“三维莫队”怎么判定一次修改是否影响当前答案?
A:维护时间指针 \(T\)。当应用/回滚第 \(k\) 次修改 \((p,\,\text{old}\to\text{new})\) 时,只有当 \(p\in[L,R]\) 才对计数结构执行“先移出旧值、再加入新值”(或相反)。否则只改底层数组,不动计数桶。
Q5:树上路径怎么做莫队?
A:Euler Tour + LCA。把树转欧拉序,每个点出现两次;指针移动时对经过的位置做“存在性翻转”(在/不在当前集合)。路径 \((u,v)\) 的答案需要对 LCA 做一次补偿。
Q6:众数为什么在莫队里常常“很难写”?
A:因为“最大频次对应的值”要在频次层里再维护候选,结构复杂、常数大。实战里常把需求改成“最大频次”“出现次数≥K 的值的个数”等,配双层桶就很稳。
Q7:MEX 如何高效维护?
A:值域分块。用 cnt[val] 统计频次,再为值域分块维护“该块中 cnt==0 的位置个数”。查询 MEX 时先定位首个非满块,再在块内线性找第一个 cnt==0。
Q8:前缀异或/哈希的题能用莫队吗?
A:能,且常见。把原问题改写成“统计子区间内满足某性质的前缀对数”,add/remove 只在计数桶里做常数增删,是莫队的高质量题型(如区间异或等于 K)。
Q9:多组数据如何避免频繁清空大数组?
A:常见办法是“时间戳数组”:用 vis[val]==stamp 判断是否使用过,换测试组直接 ++stamp,省去全清零。
Q10:为什么我把比较器写好了还是 TLE?
A:优先排查三点:① add/remove 是否含重分支或隐性 \(O(\log V)\) 操作;② 值域是否离散化充分;③ 排序是否产生了过多的“折返”。常数吃紧时改用 Hilbert/Peano。
Q11:答案溢出怎么办?
A:像“等值对数”“异或对数”上界近 \(O(n^2)\),答案必须用 64 位整型(或更大)。中间乘法也要注意提升类型。
Q12:树上莫队与 DSU on tree(小并大)怎么取舍?
A:路径类通常树上莫队更自然;子树并集类通常 DSU on tree 更顺手。若两者都能做,看谁的增删/合并常数更低。
Q13:C++ 实现还有哪些卡常细节?
A:把块号预存为整型字段;add/remove 合并分支、少做条件判断;频次与派生桶放连续大数组,尽量走顺序访问路径。
Q14:Hilbert/Peano 的参数怎么选才不炸?
A:Hilbert 的阶数覆盖最大坐标即可(常见用 64 位序值);Peano 是 \(3^k\) 网格,注意中间乘加别溢出。两者在实现上都可直接套标准模板。
Q15:什么时候该果断放弃莫队?
A:当 add/remove 无法设计成 \(O(1)\),或问题有更直接的“可合并结构”(稀疏表/线段树/分治),就别硬上了。选对工具,收益最大。
15. 训练题单(分阶进阶)
入门(把三件套写顺手)
- SPOJ:DQUERY
- Codeforces:220B Little Elephant and Array
经典
- Codeforces:86D Powerful Array (莫队名题)
- Codeforces:617E XOR and Favorite Number
- AtCoder ABC174 F:Range Set Query
带修改(三维莫队)
- Codeforces:940F Machine Learning
树上莫队(路径/子树场景)
- Codeforces:375D Tree and Queries
那些我做过的莫队好题
- Luogu P1972
- Luogu P1903
- Luogu P1494
- Luogu P3709
- Luogu P4074
16. 推荐阅读
-
Codeforces 博客:
- 《Everything on Mo's Algorithm》
- 《Mo’s Algorithm on Trees [Tutorial]》
- 《Mo's algorithm and 3D Mo》
- 《An alternative sorting order for Mo’s algorithm》(Hilbert 曲线排序)
- 《Space-Filling Curves for Mo’s Algorithm》(Peano 比较器)
-
USACO Guide:
- Square Root Decomposition(含 Mo’s Algorithm 小节与题单)

浙公网安备 33010602011771号