定义

莫队算法是一种离线处理区间查询的技巧,适用于满足「从 的答案可以 扩展到相邻区间」条件的题目。1

核心思想:将所有询问离线,对询问进行特殊排序,然后顺序处理,通过移动指针逐步扩展/收缩区间。

形式化

对于序列上的区间询问问题,如果从 的答案能够 扩展到:

则可以在 的复杂度内求出所有询问的答案。

排序方法

对于区间 ,以 所在块的编号为第一关键字, 为第二关键字从小到大排序。

struct Query {
    int l, r, id, block;
};
 
bool cmp(const Query& a, const Query& b) {
    if (a.block != b.block) return a.block < b.block;
    return a.r < b.r;
}

算法实现

int BLOCK_SIZE;
 
void move(int pos, int sign) {
    // 根据题目更新答案
}
 
vector<long long> solve(int n, int m, const vector<Query>& queries) {
    BLOCK_SIZE = static_cast<int>(sqrt(n));
    vector<Query> q = queries;
    sort(q.begin(), q.end(), cmp);
    
    vector<long long> ans(m);
    int l = 1, r = 0;  // 当前区间 [l, r]
    long long nowAns = 0;
    
    for (const auto& qu : q) {
        while (l > qu.l) move(--l, 1);
        while (r < qu.r) move(++r, 1);
        while (l < qu.l) move(l++, -1);
        while (r > qu.r) move(r--, -1);
        ans[qu.id] = nowAns;
    }
    return ans;
}

复杂度分析

设块长度为 ,则:

  • 同一块内, 的移动总代价为
  • 跨块时, 的移动总代价约为
  • 总复杂度:

时,复杂度最优为

时,取 ,复杂度为

例题:小Z的袜子

题目:求区间 内随机选两个数,它们相等的概率。

思路:莫队模板题。维护每种颜色的出现次数和当前配对方案数。

#include <bits/stdc++.h>
using namespace std;
 
struct Query {
    int l, r, id, block;
};
 
int n, m, BLOCK_SIZE;
vector<int> a;
vector<Query> queries;
vector<long long> ansNum, ansDen;
 
bool cmp(const Query& a, const Query& b) {
    if (a.block != b.block) return a.block < b.block;
    return (a.block & 1) ? a.r < b.r : a.r > b.r;  // 奇偶优化
}
 
int cnt[500005];
 
void add(int pos) {
    int c = a[pos];
    ansNum[0] += cnt[c];
    cnt[c]++;
}
 
void remove(int pos) {
    int c = a[pos];
    cnt[c]--;
    ansNum[0] -= cnt[c];
}
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> m;
    a.resize(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i];
    
    BLOCK_SIZE = static_cast<int>(sqrt(n));
    queries.resize(m);
    ansNum.resize(m);
    ansDen.resize(m);
    
    for (int i = 0; i < m; i++) {
        cin >> queries[i].l >> queries[i].r;
        queries[i].id = i;
        queries[i].block = queries[i].l / BLOCK_SIZE;
    }
    
    sort(queries.begin(), queries.end(), cmp);
    
    int l = 1, r = 0;
    for (const auto& q : queries) {
        while (l > q.l) add(--l);
        while (r < q.r) add(++r);
        while (l < q.l) remove(l++);
        while (r > q.r) remove(r--);
        
        long long len = q.r - q.l + 1;
        ansNum[q.id] = ansNum[0];
        ansDen[q.id] = len * (len - 1) / 2;
    }
    
    for (int i = 0; i < m; i++) {
        long long g = gcd(ansNum[i], ansDen[i]);
        cout << ansNum[i] / g << '/' << ansDen[i] / g << '\n';
    }
    return 0;
}

公式推导:从 个颜色 的元素中选两个,方案数为 。总方案数为 。颜色 加入时,新增配对数为 (因为可以和之前的 个配对)。

莫队的优化

奇偶性优化

在同一块内, 交替往返移动。可以让奇数块正序、偶数块逆序:

bool cmp(const Query& a, const Query& b) {
    if (a.block != b.block) return a.block < b.block;
    return (a.block & 1) ? a.r < b.r : a.r > b.r;
}

扩展:带修改莫队

当问题支持单点修改时(称为「修改莫队」),需要将询问扩展为三元组 ,其中 表示第 个修改后的状态。

块大小通常设为 ,复杂度为

应用场景

  • 区间众数、区间出现次数
  • 区间不同元素数量
  • 区间满足某种条件的对数
  • 需要离线处理的大量区间查询

参考资料

Footnotes

  1. 普通莫队算法 - OI Wiki