【知识点】莫队算法

简介

莫队算法是由中国数学家莫涛整理、提出的算法,用于解决一类离线区间询问问题。

如果有\(m\)次询问,每次询问一个区间\([l_i, r_i]\),我们之前都是对于每个区间单独进行处理,每次询问都独立地回答,互不相干。但有时,这些区间的答案是可以相互转换的。假设\([l_i, r_i]\)的答案能在\(O(1)\)内转换到\([l_i+1, r_i],[l_i-1, r_i],[l_i, r_i+1],[l_i, r_i-1]\)这四个区间,那么就可以用莫队算法在\(O(n \sqrt n)\)的复杂度内计算出所有询问的答案。

过程

先读入所有查询。

第一次查询我们可以用暴力解决,从第二次开始,每次从上一次的查询结果开始逐步转换,慢慢移动到该区间获得答案。

但移动次数可能过多。如果数据为\([1, 1], [n, n], [1, 1], [n, n], ……\)这种最坏情况,复杂度\(O(nm)\),所以我们需要对查询进行排序。

我们把\(n\)个下标分块(块长为\(\sqrt n\)较优),对询问排序时,以左端点所在的块的序号为第一关键字,以右端点的下标为第二关键字,从小到大排好序。

为什么这样排序呢?

每次移动时,左端点尽可能是在同块或者相邻块内移动。同块内移动时,因为块长为\(\sqrt n\),左端点在块内,可能的左端点下标一共也就\(\sqrt n\)个,所以左端点移动次数最多为\(\sqrt n\)

而每次同块内移动时,右端点是从小到大排好的,不一定在同一块中,移动次数最多为\(n\)

那么,每个块内的移动次数就是\(\sqrt n + n\),而每次移动都是\(O(1)\)的,一共有\(\sqrt n\)个块,总的时间复杂度为\(O(\sqrt n (\sqrt n + n)) = O(n\sqrt n)\)


实现

void move(int pos, int sign) {
  //更新答案,根据题目要求,但记住必须O(1)
}

void solve() {
  BLOCK_SIZE = int(ceil(pow(n, 0.5)));	//块长
  //……
  //预处理操作,比如分块等……
  //……
  sort(querys, querys + m, cmp);		//对查询进行排序,记得自定义排序函数
  for (int i = 0; i < m; ++i) {
    const query &q = querys[i];
    while (l > q.l) move(--l, 1);	//左边界需要左移
    while (r < q.r) move(++r, 1);	//右边界需要右移
    while (l < q.l) move(l++, -1);	//左边界需要右移
    while (r > q.r) move(r--, -1);	//右边界需要左移
    ans[q.id] = nowAns;
  }
}

注意,核心的四行(调用move的那四行)是有严格顺序的!

为什么不能随意交换顺序?

如果先让\(l\)向右,可能超过\(r\),造成正在处理空区间或者无意义区间(\(l>r\))的情况。先让\(r\)向左同理。

总之,先试图扩大区间(\(l--, r++\))再收缩区间(\(l++, r--\))这种方式一定是安全的。

还有,如果采用\(l--, r--, l++, r++\)这种顺序也是正确的。(以上两种方式的对称方式也是正确的,24种组合方式中有6种正确顺序)。


例题

洛谷P1494 小Z的袜子

思路

首先,如果一个区间\([l, r]\)内的第\(i\)个颜色的袜子数量为\(s_i\),则取出两个\(i\)色袜子的方案一共有\(c_{s_i}^2 = (s_i * (s_i - 1)) / 2\)种。那么取出两个同色袜子的方案就有:

\[\sum_{i=1}^n {(s_i * (s_i - 1)) / 2} \]

区间长度为\(r - l + 1\),那么所有的方案总数为$ ((r - l + 1) * (r - l)) / 2 $。

取出两个同色袜子的概率为:

\[\dfrac{\sum_{i=1}^n {(s_i * (s_i - 1))/2}}{(r - l + 1) * (r - l) / 2} \]

\[\]

分子分母同时乘以\(2\)

\[\dfrac{\sum_{i=1}^n {(s_i * (s_i - 1))}}{(r - l + 1) * (r - l)} \]

分子拆开得:

\[\dfrac{\sum_{i=1}^n {s_i^2} - \sum_{i=1}^n {s_i}}{(r - l + 1) * (r - l)} \]

由于各类颜色加起来总数就是区间的长度,代入\(\sum_{i=1}^n {s_i} = r - l + 1\)

\[\dfrac{\sum_{i=1}^n {s_i^2} - (r - l + 1)}{(r - l + 1) * (r - l)} \]

所以只需要维护各颜色出现次数的平方和即可。代码中用s[i]表示\(s_i\)ss表示\(\sum_{i=1}^n {s_i^2}\)

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

int n, m, color[50010], id[50010], now_l = 1, now_r = 0;
//color[i]为第i个袜子的颜色
//id[i]为第i个袜子的块的编号
i64 s[50010], ss = 0;	//s记录每种颜色出现的次数,ss记录这些次数平方的和
i64 ansa[50010], ansb[50010];

struct question {
	int l, r, idx;	//idx表示该询问的序号
	i64 a, b;	//注意分子分母用long long类型
} q[50010];

bool cmp(question x, question y) {	//标准的莫队自定义比较函数
	if (id[x.l] == id[y.l])
		return x.r < y.r;
	return id[x.l] < id[y.l];
}

inline void update(int now, int how) {
	ss -= s[color[now]] * s[color[now]];	//先减去现在的平方
	s[color[now]] += how;	//更新该颜色出现次数
	ss += s[color[now]] * s[color[now]];	//加上正确的平方
	return;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	int siz = sqrt(n);	//块长为根号n
	for (int i = 1; i <= n; i++) {
		cin >> color[i];
		id[i] = (i - 1) / siz + 1;
	}
	for (int i = 1; i <= m; i++) {
		cin >> q[i].l >> q[i].r;
		q[i].idx = i;
	}
	sort(q + 1, q + m + 1, cmp);
	for (int i = 1; i <= m; i++) {
		while (now_l > q[i].l)
			update(--now_l, 1);
		while (now_r < q[i].r)
			update(++now_r, 1);
		while (now_l < q[i].l)
			update(now_l++, -1);
		while (now_r > q[i].r)
			update(now_r--, -1);
		q[i].a = ss - (q[i].r - q[i].l + 1);	//计算分子
		q[i].b = (i64)(q[i].r - q[i].l + 1) * (i64)(q[i].r - q[i].l);	//计算分母
		if (q[i].a == 0 || q[i].l == q[i].r) {	//特判分子为0的情况
			q[i].b = 1;
			ansa[q[i].idx] = q[i].a;
			ansb[q[i].idx] = q[i].b;
			continue;
		}
		int t = __gcd(q[i].a, q[i].b);	//约分(分子分母同时除以最大公约数)
		q[i].a /= t;
		q[i].b /= t;
		ansa[q[i].idx] = q[i].a;
		ansb[q[i].idx] = q[i].b;
	}
	for (int i = 1; i <= m; i++)
		cout << ansa[i] << '/' << ansb[i] << '\n';
	return 0;
}

一些思考

更严谨的时间复杂度与更优块长

为什么时间复杂度为\(O(n\sqrt n)\),甚至都没有出现过\(m\)?难道除了排序部分的\(O(mlogm)\),计算时长与询问个数没有关系吗?

为什么块长为\(\sqrt n\)?有没有更优情况?

假设块长为\(s\),共有\(\frac{n}{s}\)个块,则时间复杂度为:

\[ms + \frac{n^2}{s} \]

根据基本不等式,当且仅当\(ms = \frac{n^2}{s}\)时,上式有最小值:

\[2\sqrt{ms * \frac{n^2}{s}} \]

化简为:

\[O(n\sqrt m) \]

即时间复杂度其实是 \(O(n\sqrt m),只不过很多题目中n\)\(m\)数量级相差不大,简写为$ O(n\sqrt n) $。

时间复杂度取最小值时,\(ms = \frac{n^2}{s}\),即\(s = \frac{n}{\sqrt m}\)

奇偶化排序

每个块中,我们的\(r\)是怎么变的?从小到大!

// 假设块的大小为 2
1 1
2 100
3 4
4 100

处理完第二个查询时,\(l = 2, r = 100\),我们明明只需要让\(l\)先增加\(2\)即可处理第四个查询,但按\(r\)递增的顺序我们选择先让\(r\)回到\(4\)处理第三个查询,再让\(r\)增大\(99\)到第四个查询,这是没有必要的。

对此,我们可以在奇数序号块时,第二关键字\(r\)按增序,偶数序号块时,第二关键字\(r\)按减序,这样r大致呈“振荡型”移动,能节省约30%的程序运行时间。

排序函数如下:

bool cmp(question x, question y) {	//奇偶化的莫队自定义比较函数
	if (id[x.l] == id[y.l] && id[x.l] % 2)
		return x.r < y.r;
	else if (id[x.l] == id[y.l])
		return x.r > y.r;
	return id[x.l] < id[y.l];
}

以下是例题不使用和使用奇偶化排序的评测记录,大家可以自行对比运行时间。

不使用奇偶化

使用奇偶化

posted @ 2025-08-12 16:28  Alkaid16  阅读(32)  评论(0)    收藏  举报