【技巧】莫队算法

一、什么是莫队算法?

莫队算法(Mo’s Algorithm) 是一种离线算法,用于高效处理 区间查询问题,特别是当这些查询可以 通过移动区间端点来增量维护答案 时。


二、核心思想

莫队算法的核心思想是:

通过对所有查询进行合理的排序,使得在处理完一个查询后,通过少量的移动操作就能转移到下一个查询的区间,从而降低整体复杂度。

这类似于 分块 + 暴力优化 的思想。


三、适用条件

莫队适用于以下类型的区间查询问题:

  1. 离线处理:所有查询已知,不需要在线回答。
  2. 可增量维护:知道 [l, r] 的答案后,能在 O(1)O(k) 时间内推出 [l±1, r][l, r±1] 的答案。
  3. 无修改操作(原始莫队):即静态数组,不支持单点或区间修改。带修改的版本称为 带修莫队(待后续扩展)。

四、基本步骤

步骤 1:分块

将整个数组分成若干块,每块大小为 B

  • 通常取块大小 B = sqrt(n),其中 n 是数组长度。
  • 也可以根据查询数量 q 调整以达到更优复杂度。
步骤 2:排序查询

将所有查询 [l, r] 按如下规则排序:

  1. 按左端点 l 所在的块编号升序排序;
  2. 若在同一块内,则按右端点 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_ll
  • 移动 cur_rr
  • 在移动过程中,每移动一步就更新答案

移动时的更新操作必须是 O(1) 的,比如:

  • 插入一个数:cnt[x]++,如果 cnt[x] == 1 则不同数个数 ++
  • 删除一个数:cnt[x]--,如果 cnt[x] == 0 则不同数个数 --

五、复杂度分析

时间复杂度
  • 左端点移动

    • 同一块内移动:每次最多移动 B = √n,共 q 次 → O(q√n)
    • 跨块跳跃:最多 √n 块,每块跳跃一次,每次 √nO(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)

nq 同阶时,约为 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 的数有多少个
  • 区间众数(较难,莫队不是最优)
  • 区间逆序对(可做,但有更好方法)
  • 树上路径颜色种类数

十四、调试技巧

  1. 打印每一步的 cur_l, cur_r, current_ans
  2. 用小数据手动模拟
  3. 检查 adddel 是否对称
  4. 确保排序正确,特别是奇偶优化逻辑

十五、总结

莫队算法是一种优雅的离线分块算法,通过 合理排序查询顺序 来减少指针移动次数,从而优化暴力。

虽然复杂度不如线段树、树状数组等数据结构优秀,但在某些难以用传统数据结构解决的区间统计问题中,莫队提供了一种 简单、直观、可扩展 的解决方案。

掌握莫队的关键在于:

  • 理解分块与排序的原理
  • 设计高效的 adddel 操作
  • 熟练使用离散化、奇偶优化等技巧
### 莫队算法简介 莫队算法是一种高效的离线查询算法,主要用于解决区间问题中的多次询问优化。其核心思想是通过对询问进行特定排序并利用双指针技巧减少不必要的重复计算,从而降低总的时间复杂度。 #### 排序规则 根据引用内容[^4],莫队算法通常会将整个数组划分为若干块,并按如下规则对询问进行排序: - 首先依据左端点所属的块编号从小到大排序; - 若左端点在同一块内,则按照右端点从小到大排序。 这种排序方式能够显著减少左右指针在处理不同询问时的移动次数,进而提高效率。 #### 时间复杂度分析 假设数组长度为 \( n \),询问数量为 \( m \),每一块大小约为 \( \sqrt{n} \) 。经过合理排序后,左右指针总共只会发生约 \( O((n+m)\sqrt{n}) \) 次移动操作。 --- ### 示例代码实现 下面提供一段基于 C++莫队算法模板代码,适用于求解区间内不同元素的数量问题: ```cpp #include <bits/stdc++.h> using namespace std; struct Query { int l, r, idx; }; bool cmp(Query &a, Query &b) { int block_a = a.l / sqrt_n; int block_b = b.l / sqrt_n; if (block_a != block_b) return block_a < block_b; return a.r < b.r; } int main(){ ios::sync_with_stdio(false); cin.tie(0); int n, q; cin >> n >> q; vector<int> arr(n+1); // 数组下标从1开始 for(int i=1;i<=n;i++) cin>>arr[i]; vector<Query> queries(q); for(auto &query : queries){ cin>>query.l>>query.r; query.idx = &query - &queries[0]; // 记录原索引位置 } double sqrt_n = sqrt(n); sort(queries.begin(), queries.end(), cmp); long long answer[q]; int current_l = 0, current_r = 0; int frequency[n+1], distinct_count = 0; memset(frequency, 0, sizeof(frequency)); auto add = [&](int pos)->void{ if(++frequency[arr[pos]] == 1) distinct_count++; }; auto remove_ = [&](int pos)->void{ if(--frequency[arr[pos]] == 0) distinct_count--; }; for(auto &query : queries){ while(current_l > query.l){add(--current_l);} while(current_r < query.r){add(++current_r);} while(current_l < query.l){remove_(current_l++);} while(current_r > query.r){remove_(current_r--);} answer[query.idx] = distinct_count; } for(int i=0;i<q;i++) cout<<answer[i]<<'\n'; return 0; } ``` 上述代码实现了基本框架结构,其中涉及到了自定义比较器 `cmp` 来完成前述提到的那种特殊排序逻辑;并通过两个辅助函数 `add` 和 `remove_` 动态更新当前活动窗口内的状态信息[^4]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值