一、什么是莫队算法?
莫队算法(Mo’s Algorithm) 是一种离线算法,用于高效处理 区间查询问题,特别是当这些查询可以 通过移动区间端点来增量维护答案 时。
二、核心思想
莫队算法的核心思想是:
通过对所有查询进行合理的排序,使得在处理完一个查询后,通过少量的移动操作就能转移到下一个查询的区间,从而降低整体复杂度。
这类似于 分块 + 暴力优化 的思想。
三、适用条件
莫队适用于以下类型的区间查询问题:
- 离线处理:所有查询已知,不需要在线回答。
- 可增量维护:知道
[l, r]的答案后,能在O(1)或O(k)时间内推出[l±1, r]或[l, r±1]的答案。 - 无修改操作(原始莫队):即静态数组,不支持单点或区间修改。带修改的版本称为 带修莫队(待后续扩展)。
四、基本步骤
步骤 1:分块
将整个数组分成若干块,每块大小为 B。
- 通常取块大小
B = sqrt(n),其中n是数组长度。 - 也可以根据查询数量
q调整以达到更优复杂度。
步骤 2:排序查询
将所有查询 [l, r] 按如下规则排序:
- 按左端点
l所在的块编号升序排序; - 若在同一块内,则按右端点
r升序排序;- 奇偶性优化:若块号为奇数,
r升序;偶数则降序(见后文优化)。
- 奇偶性优化:若块号为奇数,
排序代码示例:
sort(queries.begin(), queries.end(), [&](const Query &a, const Query &b) { if (a.l / block_size != b.l / block_size) return a.l < b.l; return (a.l / block_size & 1) ? (a.r < b.r) : (a.r > b.r); // 奇偶优化 });
步骤 3:依次处理查询
维护当前区间 [cur_l, cur_r] 和当前答案 ans。
对于每个查询 [l, r]:
- 移动
cur_l到l - 移动
cur_r到r - 在移动过程中,每移动一步就更新答案
移动时的更新操作必须是 O(1) 的,比如:
- 插入一个数:
cnt[x]++,如果cnt[x] == 1则不同数个数++ - 删除一个数:
cnt[x]--,如果cnt[x] == 0则不同数个数--
五、复杂度分析
时间复杂度
-
左端点移动:
- 同一块内移动:每次最多移动
B = √n,共q次 →O(q√n) - 跨块跳跃:最多
√n块,每块跳跃一次,每次√n→O(n) - 总计:
O((q+√n)√n) ≈ O(q√n + n)
- 同一块内移动:每次最多移动
-
右端点移动:
- 每个块内
r单调递增(或奇偶优化后波动小),所以每个块中r最多移动n - 共
√n个块 →O(n√n)
- 每个块内
✅ 总时间复杂度:O(n√n + q√n),通常写作 O((n + q)√n)
当 n 和 q 同阶时,约为 O(n√n)
空间复杂度:O(n),用于存储数组、计数数组等。
六、经典例题:区间不同数的个数
给定数组 a[1..n],q 次询问 [l, r] 内不同数字的个数。
示例输入:
n = 6, q = 3
a = [1, 2, 3, 1, 2, 3]
查询:
[1, 3] -> {1,2,3} -> 3
[2, 5] -> {2,3,1} -> 3
[1, 6] -> {1,2,3} -> 3
七、C++ 实现(完整代码)
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
// 定义查询结构体
struct Query {
int l, r; // 查询区间 [l, r](1-indexed)
int id; // 查询编号,用于输出答案
};
// 全局变量
const int MAXN = 100005;
int n, q; // 数组长度,查询数量
int a[MAXN]; // 原数组
int block_size; // 块大小
vector<Query> queries; // 查询列表
int cnt[MAXN * 2]; // 计数数组(注意值域可能负数,可偏移)
long long current_ans; // 当前区间答案(这里是不同数个数)
vector<long long> result; // 存储每个查询的答案
// 添加位置 pos 的元素
void add(int pos) {
if (cnt[a[pos]] == 0) {
current_ans++;
}
cnt[a[pos]]++;
}
// 删除位置 pos 的元素
void del(int pos) {
cnt[a[pos]]--;
if (cnt[a[pos]] == 0) {
current_ans--;
}
}
// 莫队主算法
void mo_algorithm() {
// 初始化
memset(cnt, 0, sizeof(cnt));
current_ans = 0;
result.resize(q);
// 当前维护的区间
int cur_l = 1, cur_r = 0;
// 遍历每个查询
for (const Query& query : queries) {
int l = query.l, r = query.r;
// 扩展或收缩右端点
while (cur_r < r) {
cur_r++;
add(cur_r);
}
while (cur_r > r) {
del(cur_r);
cur_r--;
}
// 扩展或收缩左端点
while (cur_l < l) {
del(cur_l);
cur_l++;
}
while (cur_l > l) {
cur_l--;
add(cur_l);
}
// 保存当前查询的答案
result[query.id] = current_ans;
}
}
// 自定义排序函数(带奇偶性优化)
bool cmp(const Query &a, const Query &b) {
int block_a = a.l / block_size;
int block_b = b.l / block_size;
if (block_a != block_b) {
return block_a < block_b;
}
// 奇偶性优化:奇数块 r 升序,偶数块 r 降序
return (block_a & 1) ? (a.r < b.r) : (a.r > b.r);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 输入
cout << "请输入数组长度 n 和查询次数 q: ";
cin >> n >> q;
cout << "请输入 " << n << " 个整数: ";
for (int i = 1; i <= n; i++) {
cin >> a[i];
// 可选:离散化处理,如果值域大但 n 小
}
// 设置块大小
block_size = static_cast<int>(sqrt(n));
if (block_size == 0) block_size = 1;
// 输入查询(1-indexed)
queries.resize(q);
for (int i = 0; i < q; i++) {
cin >> queries[i].l >> queries[i].r;
queries[i].id = i;
}
// 排序查询
sort(queries.begin(), queries.end(), cmp);
// 运行莫队
mo_algorithm();
// 输出结果
cout << "\n各查询结果如下:\n";
for (int i = 0; i < q; i++) {
const Query& query = queries[i];
cout << "查询 [" << query.l << ", " << query.r << "] 的答案是: " << result[query.id] << '\n';
}
return 0;
}
八、奇偶性优化(Odd-Even Optimization)
为什么需要?
在排序时,若同一块内 r 都升序,则从一个查询跳到下一个时,r 会一直向右走,但 下一个块的第一个查询的 r 可能很小,导致 r 大幅左移。
优化方法:
- 奇数块:
r升序 - 偶数块:
r降序
这样,奇数块结尾时 r 在最右,偶数块从右往左处理,到最左,然后跳到下一个奇数块时 r 又从左边开始向右走,减少了 r 的来回跳跃。
实测可提升约 20%~30% 性能。
九、值域处理与离散化
若原数组值域很大(如 1e9),但 n 较小(如 1e5),需先离散化:
vector<int> disc = vector<int>(a + 1, a + n + 1);
sort(disc.begin(), disc.end());
disc.erase(unique(disc.begin(), disc.end()), disc.end());
for (int i = 1; i <= n; i++) {
a[i] = lower_bound(disc.begin(), disc.end(), a[i]) - disc.begin() + 1;
}
然后 cnt 数组大小只需 disc.size()。
十、扩展:带修莫队(Mo’s Algorithm with Updates)
支持单点修改的莫队,引入 时间维度,每个查询带上“最后一次修改的时间戳”。
- 状态变为
(l, r, t) - 排序规则:
l分块,r分块,t升序 - 移动时不仅要移动
l, r,还要执行或撤销修改操作
复杂度:O(n^(5/3)),块大小取 n^(2/3)
十一、树上莫队(Tree Mo’s Algorithm)
将树的 DFS 序转化为序列问题,利用欧拉序(入栈出栈序列)处理路径查询。
关键:利用 LCA 判断路径,通过奇偶位置判断节点是否在路径上。
十二、优缺点总结
| 优点 | 缺点 |
|---|---|
| 思想直观,代码模板化 | 只能离线 |
| 适用范围广(可增量维护) | 复杂度较高,O(n√n) |
| 易于扩展(带修、树上) | 常数较大,对 n > 1e5 可能超时 |
| 不依赖高级数据结构 | 需要精心设计 add/del 操作 |
十三、适用题目类型
- 区间不同数个数
- 区间出现次数为
k的数有多少个 - 区间众数(较难,莫队不是最优)
- 区间逆序对(可做,但有更好方法)
- 树上路径颜色种类数
十四、调试技巧
- 打印每一步的
cur_l, cur_r, current_ans - 用小数据手动模拟
- 检查
add和del是否对称 - 确保排序正确,特别是奇偶优化逻辑
十五、总结
莫队算法是一种优雅的离线分块算法,通过 合理排序查询顺序 来减少指针移动次数,从而优化暴力。
虽然复杂度不如线段树、树状数组等数据结构优秀,但在某些难以用传统数据结构解决的区间统计问题中,莫队提供了一种 简单、直观、可扩展 的解决方案。
掌握莫队的关键在于:
- 理解分块与排序的原理
- 设计高效的
add和del操作 - 熟练使用离散化、奇偶优化等技巧
1880

被折叠的 条评论
为什么被折叠?



