写在前面
首先,莫队应该很久以前写的,为什么咕到了省选之后呢?主要是 5ab 发现自己连莫队都还不会,所以就来补笔记+补题了。
u1s1,莫队是一个十分神奇的算法,这是一个离线处理的范例,也是每次考数据结构必卡的。所以学一学莫队没什么坏处,写起来简单易懂,说不定还能卡得过去骗不少分。
关于莫队
什么是莫队?按照 LJ 的说法:
莫队就是优雅的暴力。
这可能很玄乎,我们先来康一个例子。
给定序列 (a),求 ([l,r]) 中不同数的数量。
暴力当然是每一次扫区间,统计是否有相同的数。当然出题人肯定不会让暴力过,所以我们要优化。
注意到不少的暴力都会重复扫区间,所以我们很自然地就会问:能不能利用前面已经完成计算的结果?这和 KMP 是如出一辙的,当然就连解决方案也是类似的。
区间移动操作
假设我们在区间 ([l,r]) 上完成了暴力操作,即得到了区间中每一个数的个数(假设已经完成了离散化,并且记录在 (cnt) 数组中)。接下来我们想要得到区间 ([l^prime, r^prime]) 的 (cnt) 数组,我们应该如何操作?
显而易见,我们可以移动 (r) 到 (r^prime),这中间要经过若干次的增加节点和删除节点的过程。如图:
同理,我们可以将 (l) 移动到 (l^prime)。这样我们就可以移动区间。这是莫队的核心操作。
优化,优化,优化!
现在有一个问题,我们能够这样移动区间,但是并没有效率提升,我们又应该如何优化呢?
有一个显而易见的优化,那就是离线将左端点升序排序,这样左端点的遍历就是 (mathcal{O}(n)) 的,会节省一点常数。
但是,只需要一组右端点反复横跳的数据,就能将这个做法卡掉。我们要怎么优化呢?别忘了——分块大法好!
我们可以对左端点分块,设块大小为 (B),数列长为 (N)。在排序时采用如下策略:
- 如果左端点都在同一块内,则根据右端点排序;
- 如果左端点不在同一块内,那么根据左端点排序。
这样的优点显而易见——既避免了右端点的反复横跳,又利用了排序单调的性质。那么——复杂度如何呢?
对于第一种情况,则左端点变化最大为 (B),而右端点从 (1) 到 (N)。如果有 (M) 组询问(假设 (N),(M) 同阶),则复杂度为 (mathcal{O}(MB+N));
对于第二种情况,我们只需要考虑块与块之间的衔接:单次复杂度为 (mathcal{O}(N)),整体复杂度为 (mathcal{O}left(dfrac{NM}{B} ight))。
综合上述两种情况,我们发现,当 (B=sqrt{N}) 时,总复杂度为 (mathcal{O}(Msqrt{N}+N)sim O(Nsqrt{N})),也就是常见的莫队复杂度。
这就是莫队,一个优化了排序的离线区间算法。
例题
数列找不同
link: https://www.luogu.com.cn/problem/P3901。
一道比较裸的莫队模板题。每一次在更新时检查是否有重复即可。
Code:
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int max_n = 100000, max_bk = 400; // 块长
struct ass
{
int l, r, id;
bool operator<(const ass& a) const
{
return ((l / max_bk == a.l / max_bk)? (r < a.r):(l < a.l)); // 莫队排序
}
};
ass qrs[max_n];
int cnt[max_n+1], a[max_n], mrs = 0; // mrs:出现次数 >= 2 的数的个数
bool ans[max_n];
#define gc getchar
inline int read()
{
int c = gc(), t = 1, n = 0;
while (isspace(c)) { c = gc(); }
if (c == '-') { t = -1, c = gc(); }
while (isdigit(c)) { n = n * 10 + c - '0', c = gc(); }
return n * t;
}
#undef gc
inline void add(int id) // 加入
{
cnt[a[id]]++;
if (cnt[a[id]] == 2)
mrs++;
}
inline void del(int id) // 移除
{
cnt[a[id]]--;
if (cnt[a[id]] == 1)
mrs--;
}
int main()
{
int n = read(), q = read(), cl = 1, cr = 0;
for (int i = 0; i < n; i++)
a[i] = read();
for (int i = 0; i < q; i++)
qrs[i].l = read() - 1, qrs[i].r = read() - 1, qrs[i].id = i;
sort(qrs, qrs + q); // 离线排序询问
for (int i = 0; i < q; i++)
{
while (cl > qrs[i].l)
add(--cl);
while (cr < qrs[i].r)
add(++cr);
while (cl < qrs[i].l)
del(cl++);
while (cr > qrs[i].r)
del(cr--); // 移动区间
ans[qrs[i].id] = !mrs; // 是否两两不同
}
for (int i = 0; i < q; i++) // 重新根据输入顺序输出答案
{
if (ans[i])
puts("Yes");
else
puts("No");
}
return 0;
}
小 Z 的袜子
link: https://www.luogu.com.cn/problem/P1494
被很多人尊为莫队模板题,但还不是那么明显。需要推一下式子((l) 代表区间长度):
这样,一切都明白了——我们需要维护区间中每一个数出现次数的平方和。这对莫队就是小菜一碟。
Code:
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
typedef long long ll;
const int max_n = 50000, max_m = 50000, max_bk = 250;
struct ass
{
int l, r, id;
bool operator<(const ass& as) const
{
return (l/max_bk == as.l/max_bk)? (((l/max_bk) & 1)? (r < as.r):(r > as.r))
:(l < as.l); // 这是一个奇怪的优化
}
};
int a[max_n], cnt[max_n+1] = {};
ll afm[max_m], afz[max_m], sum = 0;
ass qrs[max_m];
ll my_gcd(ll a, ll b) { return b? my_gcd(b, a%b):a; }
inline ll sq(ll x) { return x * x; }
#define gc getchar
inline int read()
{
int c = gc(), t = 1, n = 0;
while (isspace(c)) { c = gc(); }
if (c == '-') { t = -1, c = gc(); }
while (isdigit(c)) { n = n * 10 + c - '0', c = gc(); }
return n * t;
}
#undef gc
inline void add(int id)
{
sum -= sq(cnt[a[id]]);
cnt[a[id]]++;
sum += sq(cnt[a[id]]);
}
// add 和 del 一行之差
inline void del(int id)
{
sum -= sq(cnt[a[id]]);
cnt[a[id]]--;
sum += sq(cnt[a[id]]);
}
int main()
{
int n = read(), m = read(), cl = 1, cr = 0;
ll tgc;
for (int i = 0; i < n; i++)
a[i] = read();
for (int i = 0; i < m; i++)
qrs[i].l = read() - 1, qrs[i].r = read() - 1, qrs[i].id = i;
sort(qrs, qrs + m);
for (int i = 0; i < m; i++)
{
while (cl > qrs[i].l)
add(--cl);
while (cr < qrs[i].r)
add(++cr);
while (cl < qrs[i].l)
del(cl++);
while (cr > qrs[i].r)
del(cr--);
afm[qrs[i].id] = 1ll * (qrs[i].r - qrs[i].l) * (qrs[i].r - qrs[i].l + 1);
afz[qrs[i].id] = 1ll * sum - (qrs[i].r - qrs[i].l + 1);
}
for (int i = 0; i < m; i++)
{
if (afz[i] != 0)
{
tgc = my_gcd(afz[i], afm[i]);
printf("%lld/%lld
", afz[i] / tgc, afm[i] / tgc);
}
else
puts("0/1"); // 特判
}
return 0;
}