【学习笔记】莫队入门

写在前面

首先,莫队应该很久以前写的,为什么咕到了省选之后呢?主要是 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) 代表区间长度):

[egin{aligned} E=&dfrac{sum cnt_i(cnt_i-1)}{l(l-1)}\ =&dfrac{sum cnt_i^2-sum cnt_i}{l^2-l}\ =&dfrac{sum cnt_i^2-l}{l^2-l} end{aligned} ]

这样,一切都明白了——我们需要维护区间中每一个数出现次数的平方和。这对莫队就是小菜一碟。

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;
}

非常感谢您读完此文章。

如果您喜欢这篇文章,请点一个赞支持一下博主。

如果您对此文有意见,欢迎在评论区留下你的看法,5ab 会尽力达到。

如果您真的不喜欢这篇文章,也请不要喷,5ab 欢迎私信反馈意见。

原文地址:https://www.cnblogs.com/5ab-juruo/p/notes-moqueue.html