欢迎光临
我们一直在努力

打卡——基础莫队算法

1. 引言

莫队算法(Mo’s Algorithm)是一种用于高效处理离线区间查询问题的优雅算法。它由莫涛在 2009 年提出,通过巧妙地调整查询顺序,将时间复杂度从 O(n²) 优化至 O(n√n) 级别,成为解决大量静态区间查询问题的利器。

本文将系统性地介绍基础莫队算法的核心思想、实现步骤、复杂度分析,并通过经典例题(如区间内不同数字的个数)进行实战演练,帮助读者彻底掌握这一算法。

2. 算法核心思想

莫队算法的核心在于 “分块” 与 “离线处理”。

2.1 问题模型

我们通常面对的问题是:给定一个长度为 n 的序列 a[1…n],以及 m 个离线查询,每个查询询问区间 [l, r] 的某个属性(如不同元素个数、区间和、众数等)。我们需要高效地回答所有查询。

2.2 暴力法的瓶颈

最直接的方法是对于每个查询,扫描整个区间 [l, r] 进行计算。总时间复杂度为 O(mn),在 n 和 m 达到 10⁵ 级别时完全不可行。

2.3 莫队的优化思路

莫队算法观察到:相邻查询的区间往往有大量重叠部分。如果我们能从一个查询的答案 [l, r] 快速推导出下一个查询的答案 [l’, r’],就能避免大量重复计算。

具体做法:

  • 将序列分成大小为 B 的块(通常 B = √n)。
  • 将所有查询按 左端点所在的块编号 为第一关键字排序,右端点 为第二关键字排序。
  • 按此顺序依次处理查询,通过移动当前区间的左右指针 l, r 来“滑”到目标区间,并维护当前答案。
  • 3. 算法实现步骤

    3.1 数据结构准备

    struct Query {
    int l, r, id; // 查询区间 [l, r] 和查询编号
    };

    int n, m; // 序列长度,查询个数
    int a[MAXN]; // 原序列
    Query q[MAXM]; // 查询数组
    int block_size; // 块大小
    int ans[MAXM]; // 存储每个查询的答案
    int cur_ans; // 当前区间 [cur_l, cur_r] 的答案
    int cnt[MAXV]; // 计数数组,用于维护当前区间内每个值的出现次数(以统计不同数字为例)

    3.2 排序函数

    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;
    // 奇偶化排序优化:奇数块右端点升序,偶数块右端点降序,减少指针移动
    return (block_a & 1) ? (a.r < b.r) : (a.r > b.r);
    }

    3.3 区间移动操作(以统计不同数字个数为例)

    void add(int pos) {
    // 将位置 pos 的元素加入当前区间
    if (cnt[a[pos]] == 0) cur_ans++; // 之前没出现过,不同数字数+1
    cnt[a[pos]]++;
    }

    void del(int pos) {
    // 将位置 pos 的元素从当前区间移除
    cnt[a[pos]];
    if (cnt[a[pos]] == 0) cur_ans; // 该数字出现次数归零,不同数字数-1
    }

    3.4 主算法流程

    void mo() {
    block_size = sqrt(n); // 设置块大小
    sort(q, q + m, cmp); // 对查询排序

    int cur_l = 1, cur_r = 0; // 当前维护的区间,初始为空区间 [1, 0]
    cur_ans = 0;

    for (int i = 0; i < m; i++) {
    int l = q[i].l, r = q[i].r;

    // 移动左指针 cur_l 到目标 l
    while (cur_l > l) add(cur_l);
    while (cur_l < l) del(cur_l++);

    // 移动右指针 cur_r 到目标 r
    while (cur_r < r) add(++cur_r);
    while (cur_r > r) del(cur_r);

    ans[q[i].id] = cur_ans; // 记录答案
    }
    }

    4. 复杂度分析

    4.1 时间复杂度

    • 左指针移动:对于每个查询,左指针最多移动 O(√n) 次(因为左端点在同一个块内)。共有 m 个查询,所以总移动次数为 O(m√n)。
    • 右指针移动:对于每个块,右指针单调移动,总移动次数为 O(n)。共有 O(√n) 个块,所以总移动次数为 O(n√n)。
    • 总复杂度:O((n+m)√n)。当 n 和 m 同阶时,约为 O(n√n)。

    4.2 空间复杂度

    • 需要存储原序列、查询和答案数组,以及辅助计数数组,总空间复杂度为 O(n + m + max_value_range)。

    5. 经典例题:区间不同数字个数

    问题描述:给定一个长度为 n 的序列,m 次询问,每次询问区间 [l, r] 内有多少个不同的数字。

    解决方案:这正是基础莫队的模板题。我们使用上面给出的代码框架,add 和 del 函数维护当前区间内每个数字的出现次数 cnt[] 以及不同数字的个数 cur_ans。

    完整代码示例:

    #include <bits/stdc++.h>
    using namespace std;

    const int MAXN = 30005;
    const int MAXM = 200005;
    const int MAXV = 1000005;

    int n, m, block_size;
    int a[MAXN], ans[MAXM], cnt[MAXV], cur_ans;

    struct Query {
    int l, r, id;
    } q[MAXM];

    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;
    return (block_a & 1) ? (a.r < b.r) : (a.r > b.r);
    }

    void add(int pos) {
    if (cnt[a[pos]] == 0) cur_ans++;
    cnt[a[pos]]++;
    }

    void del(int pos) {
    cnt[a[pos]];
    if (cnt[a[pos]] == 0) cur_ans;
    }

    int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);

    scanf("%d", &m);
    for (int i = 0; i < m; i++) {
    scanf("%d %d", &q[i].l, &q[i].r);
    q[i].id = i;
    }

    block_size = sqrt(n);
    sort(q, q + m, cmp);

    int cur_l = 1, cur_r = 0;
    cur_ans = 0;

    for (int i = 0; i < m; i++) {
    int l = q[i].l, r = q[i].r;
    while (cur_l > l) add(cur_l);
    while (cur_l < l) del(cur_l++);
    while (cur_r < r) add(++cur_r);
    while (cur_r > r) del(cur_r);
    ans[q[i].id] = cur_ans;
    }

    for (int i = 0; i < m; i++) printf("%d\\n", ans[i]);
    return 0;
    }

    赞(0)
    未经允许不得转载:171主机测评 » 打卡——基础莫队算法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址