莫队算法
莫队算法是一种由莫涛提出的基于平方根分解思想离线处理区间查询问题的算法,其时间复杂度通常为 \(O((N+Q) \sqrt{N})\),其中 \(N\) 是数组大小,\(Q\) 是查询次数。
莫队算法成功的关键在于它不按原始顺序处理查询,而是根据索引进行离线排序。首先将所有左端点在第 1 块的查询处理完,接着处理左端点在第 2 块的查询,以此类推。对于左端点位于同一块内的查询,按照它们的右端点进行升序排列。
莫队算法通过不断“扩展”或“收缩”当前区间来响应一个查询,算法始终只维护一个存储当前区间信息的数据结构(初始为空)。当需要回答下一个查询时,通过在当前区间的两端逐个添加或移除元素,直到当前区间转化为查询所要求的区间,这种方式将复杂的区间查询转化为了一系列简单的“添加/删除单个元素”操作。
由于算法必须重新排列查询的顺序,因此它仅适用于允许离线处理的问题(即可以预先获取所有查询请求)。
在莫队算法中,使用两个函数,分别用于向当前维护的区间中添加和删除元素。
int n, m; // m: 询问的数量
struct Query {
int l, r, id;
bool operator<(const Query& other) const {
int b1 = l / B, b2 = other.l / B;
if (b1 != b2) return b1 < b2;
return r < other.r;
}
};
Query q[N];
int ans[N];
void add(int pos);
void del(int pos);
int get(); // 从数据结构中提取答案
void solve() {
sort(q, q + m);
int x = 0, y = -1; // 数据结构将始终维护区间 [x,y] 中的信息
for (int i = 0; i < m; i++) {
int l = q[i].l, r = q[i].r, id = q[i].id;
while (x > l) add(--x);
while (y < r) add(++y);
while (x < l) del(x++);
while (y > r) del(y--);
ans[id] = get();
}
}
在莫队算法中,需要根据具体问题的需求,灵活选择底层数据结构并修改 add、del 和 get 这三个核心函数。
对于简单的区间求和问题,目标是实时维护当前区间的数值总和。
- 数据结构:使用一个简单的整型变量(如
sum),初始值为 0。 add函数:将当前位置的数值累加到sum变量中,并更新答案。del函数:从sum变量中减去当前位置的数值,并更新答案。get函数:直接返回该整型变量的值。
如果是查询区间众数,需要维护元素出现的频次及排序。
- 数据结构:使用两个平衡二叉搜索树(在 C++ 中可用
map或set实现),第一棵树(如map<int, int>) 存储当前区间内每个数字的出现次数,第二棵树(如set<pair<int, int>>) 存储“频次-数值”对,并按频次排序。 add函数:从第二棵树中移除该数字旧的频次记录,在第一棵树中增加该数字的频次计数,将更新后的频次记录重新插入第二棵树。del函数:执行与add类似的逻辑,但改为减少第一棵树中的频次计数。get函数:直接查看第二棵树(由于已排序),在 \(O(1)\) 时间内返回当前频次最高的最优值。
莫队算法的高效性源于对查询区间的合理排序,从而最小化了双指针移动的总次数。
在处理任何查询之前,需要对所有的 \(Q\) 个询问进行离线排序,排序的时间复杂度为 \(O(Q \log Q)\)。
假设分块的大小为 \(B\),分别考虑右指针和左指针的移动情况。
- 对于左端点落在同一个块内的所有询问,右端点是按升序排列的。因此,在处理这一个块内的所有询问时,右指针总共只会移动 \(O(N)\) 次。总共有 \(\dfrac{N}{B}\) 个块,因此,右指针在所有块中的总移动次数为 \(O(\dfrac{N}{B} \cdot N)\)。
- 在两个相邻的询问之间,左端点由于被限制在同一个块内(或相邻块),其变化量最大为 \(O(B)\)。
- 总共有 \(Q\) 个询问,因此左指针的总移动次数为 \(O(B \cdot Q)\)。
当块大小取最优值 \(B \approx \sqrt{N}\) 时,总的操作次数(即指针移动次数)为 \(O((N+Q) \sqrt{N})\)。最终时间复杂度为 \(O((N+Q) \cdot F \sqrt{N})\),其中 \(O(F)\) 代表执行一次 add 或 del 函数的时间复杂度。
奇偶块排序优化是莫队算法中常用的“常数优化”手段,考虑到在处理完一个块的查询后,右指针通常位于数组末尾,进入下一个块时,如果右端点再次从头开始排序,右指针可能会跨越整个数组回到前端。如果在奇数块中,将右端点按升序排序,在偶数块中,将右端点按降序排列,那么右指针在完成一个块的操作后,可以直接顺势处理下一个块的右端点,无需“重置”回起点,从而减少了约一半的右指针移动距离。
例题:P1494 [国家集训队] 小 Z 的袜子
给定一个长度为 \(N \ (N \le 50000)\) 的序列 \(C_i \ (C_i \le N)\),表示 \(N\) 只袜子的颜色。有 \(M \ (M \le 50000)\) 次询问,每次询问区间 \([L,R]\)。要求计算在区间内随机选出两只袜子,它们颜色相同的概率,结果需化为最简分数,若 \(L=R\),输出
0/1。
对于区间 \([L,R]\),设其长度为 \(l = R-L+1\)。从 \(l\) 只袜子里选 2 只,方案数为 \(C_l^2 = \dfrac{l \cdot (l-1)}{2}\)。设区间内第 \(i\) 种颜色出现了 \(c_i\) 次,则选出两只颜色均为 \(i\) 的方案数为 \(C_{c_i}^2 = \dfrac{c_i \cdot (c_i-1)}{2}\)。总的相同颜色方案数为 \(\sum \dfrac{c_i \cdot (c_i-1)}{2} = \dfrac{\sum (c_i^2 - c_i)}{2} = \dfrac{ \left(\sum c_i^2\right) - l}{2}\)。因此,所求概率为 \(P = \dfrac{\frac{\left(\sum c_i^2 \right) - l}{2}}{ \frac{l \cdot (l-1)}{2} } = \dfrac{ \left( \sum c_i^2 \right) - l }{l \cdot (l-1)}\)。
只需要使用莫队算法动态维护区间内各颜色出现次数的平方和 \(\sum c_i^2\) 即可,时间复杂度为 \((N+M) \sqrt{N}\)。
参考代码
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
using ll = long long;
const int N = 50005;
const int B = 256;
int c[N], cnt[N];
ll ans[N][2];
struct Query {
int l, r, id;
bool operator<(const Query& other) const {
int b1 = l / B, b2 = other.l / B;
if (b1 != b2) return b1 < b2;
// 奇偶优化:左端点所在块相同时,奇数块右端点升序,偶数块右端点降序
if (b1 & 1) return r < other.r;
return r > other.r;
}
};
Query q[N];
ll sum;
void add(int pos) {
int v = c[pos];
sum -= 1ll * cnt[v] * cnt[v];
cnt[v]++;
sum += 1ll * cnt[v] * cnt[v];
}
void del(int pos) {
int v = c[pos];
sum -= 1ll * cnt[v] * cnt[v];
cnt[v]--;
sum += 1ll * cnt[v] * cnt[v];
}
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a;
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
for (int i = 0; i < m; i++) {
scanf("%d%d", &q[i].l, &q[i].r);
q[i].id = i;
}
sort(q, q + m);
int x = 1, y = 0;
for (int i = 0; i < m; i++) {
int l = q[i].l, r = q[i].r, id = q[i].id;
if (l == r) {
ans[id][0] = 0; ans[id][1] = 1;
continue;
}
while (x > l) add(--x);
while (y < r) add(++y);
while (x < l) del(x++);
while (y > r) del(y--);
int len = r - l + 1;
ll num = sum - len, den = 1ll * len * (len - 1);
if (num == 0) {
ans[id][0] = 0; ans[id][1] = 1;
} else {
ll g = gcd(num, den);
ans[id][0] = num / g;
ans[id][1] = den / g;
}
}
for (int i = 0; i < m; i++) printf("%lld/%lld\n", ans[i][0], ans[i][1]);
return 0;
}

浙公网安备 33010602011771号