Y
K
N
U
F

在线莫队学习笔记

前言

其实刚学莫队的时候就听说这个了(似乎叫诗乃莫队),一直想学来着,在学了主席树后更想学了,然后……

一直到现在我自己造了出来我都没学,很好造,你来你也行。

正文

例题:疯狂的颜色序列

一句话题意:区间数颜色,在线。

这个是出现在我们学校题库主席树专题里的题目,当时就非常想写莫队,可惜啊……

不过现在也来得及。

说是莫队,其实在线莫队在我看来就是个分块,但是不管是预处理的部分还是查询的部分都和莫队十分相似,所以称为莫队也好。

令块长为 \(B\),大概的过程如下:

  1. 预处理每块的“莫队数组”,在这题也就是每个颜色的出现次数,复杂度为 \(O(n\frac nB)\)\(O(n\frac {n^2} {B^2})\),原因一会另讲,这题可以做到 \(O(n\frac nB)\)
  2. 预处理块与块间的贡献,复杂度是 \(O(n\frac nB)\)
  3. 整块间贡献直接查,然后散块用“莫队数组”辅助求出贡献,复杂度 \(O(mB)\)

然后就没了,显然这题 \(B\) 取到 \(\sqrt n\) 最优。

在线莫队的空间复杂度一般很劣,会到达 \(O(n\sqrt n)\),实在不行也许可以考虑 unordered_map 并祈祷别卡你。

可以思考一下怎么实现,相信读者一定是会的。

不会做也没关系的说,下面带着代码再来过一遍流程:

for(int i = 1; md[i - 1].pos != n; i ++) {
	int L = B * (i - 1) + 1;
	md[i].pos = std::min(n, B * i);
	for(int j = L; j <= md[i].pos; j ++) lp[j] = i - 1, rp[j] = i; 
	for(int j = 1; j <= md[i].pos; j ++) ++ md[i].cnt[a[j]];
}

\(pos\) 是每块的右端点,\(lp,rp\) 表示每个点左一个,右一个块,不做解释。

然后是 \(cnt\),这个就是所讲的“莫队数组”,如果满足可减的性质就可以直接相减来求出两块间这个东西,否则就只能每两块间单独处理,这就是为什么复杂度有时候 \(O(n\frac nB)\)\(O(n\frac {n^2} {B^2})\)

for(int i = 1; md[i - 1].pos != n; i ++) {
	int ans = 0;
	for(int j = i + 1; md[j - 1].pos != n; j ++) {
		for(int k = md[j - 1].pos + 1; k <= md[j].pos; k ++) 
			if(++ cnt[a[k]] == 1) ans ++;
		mas[i][j] = ans;
	}
	memset(cnt, 0, sizeof cnt);
}

\(mas\) 记录的是块间的答案,也就是所谓查询恰好在每块两端的答案,相当于是记录了很多个莫队的状态,可以保证不管查询哪一个 \(l,r\) 都可以只移动 \(B\) 次指针而得到结果。

int lk = rp[L], rk = lp[R];
l = md[lk].pos, r = md[rk].pos + 1;
ans = mas[lk][rk];
for(int i = L; i <= l; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]];
for(int i = r; i <= R; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]];
for(int i = L; i <= l; i ++) if(++ cnt[a[i]] == 1) ans ++;
for(int i = r; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++;

这个是查询,\(lk,rk\) 代表 \(L,R\) 最靠近的块,然后 \(l,r\) 则代表莫队的两个指针,接下来四个循环,前两个处理要用的莫队数组,后两个模拟莫队指针移动过程,然后就对完了。

这题我是没法给出评测地址什么的了,就把 AC 代码放这了,比主席树短得多。

code

// code by 樓影沫瞬_Hz17
#include <bits/stdc++.h>

constexpr int N = 500000 + 10;

constexpr int B = 750, T = N / B + 10;

struct Moduis {
	int pos;
	int cnt[50001];
} md[T];
int mas[T][T];
int cnt[50001], a[N];
int lp[N], rp[N];
int n, m;

int main() {
	using std::cin; using std::cout;
	std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m;
	std::vector<int> v;
	for(int i = 1; i <= n; i ++) cin >> a[i], v.push_back(a[i]);
	std::sort(v.begin(), v.end()), v.erase(std::unique(v.begin(), v.end()), v.end());
	for(int i = 1; i <= n; i ++) a[i] = std::lower_bound(v.begin(), v.end(), a[i]) - v.begin();
	for(int i = 1; md[i - 1].pos != n; i ++) {
		int L = B * (i - 1) + 1;
		md[i].pos = std::min(n, B * i);
		for(int j = L; j <= md[i].pos; j ++) lp[j] = i - 1, rp[j] = i; 
		for(int j = 1; j <= md[i].pos; j ++) ++ md[i].cnt[a[j]];
	}
	for(int i = 1; md[i - 1].pos != n; i ++) {
		int ans = 0;
		for(int j = i + 1; md[j - 1].pos != n; j ++) {
			for(int k = md[j - 1].pos + 1; k <= md[j].pos; k ++) 
				if(++ cnt[a[k]] == 1) ans ++;
			mas[i][j] = ans;
		}
		memset(cnt, 0, sizeof cnt);
	}
	for(int c = 1, ans = 0, L, R, l, r; c <= m; c ++) {
		cin >> L >> R; L = (L + ans) % n + 1, R = (R + ans) % n + 1;
		if(L > R) std::swap(L, R);
		if(rp[L] == rp[R]) {
			ans = 0;
			for(int i = L; i <= R; i ++) cnt[a[i]] = 0;
			for(int i = L; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++;
		} 
		else {
			int lk = rp[L], rk = lp[R];
			l = md[lk].pos, r = md[rk].pos + 1;
			ans = mas[lk][rk];
			for(int i = L; i <= l; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]];
			for(int i = r; i <= R; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]];
			for(int i = L; i <= l; i ++) if(++ cnt[a[i]] == 1) ans ++;
			for(int i = r; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++;
		}
		cout << ans << '\n';
	}
}   
// 星間~ 干渉~ 融解~ 輪迴~ 邂逅~ 再生~ ララバイ~

例题(?):更疯狂的颜色序列(没这题,乱说的)

注意到,并不存在这个题目。

和上面一样的题目,同样在线,多了一个单点修改。

如果有一个修改,我们的 \(cnt\)\(mas\) 竟然全线崩溃了!都重构的话岂不是慢完了!!
我们发现这个很让人苦恼,难道只能用上高级树形数据结构了吗?

啊,相信大家都可以一瞬间想到怎么办对吧。

\(cnt\) 数组只有 \(\frac nB\) 个,暴力修改就是对的,而 \(mas\) 竟然有 \(\frac{n^2}{B^2}\) 个,这很可怕。这里介绍一个摆烂的和一个不摆烂的解决方案。

摆烂点,注意到我们只会用到 \(m\)\(mas\),直接赖着不改,,存下来,用到了再说。真的用到了就看看这个块的答案有没有被影响,一共检查上次检查到现在的修改了的颜色个数次,这点只要存储修改和每个块的修改时间戳就可以做到,发现最劣是 \(O(nm)\) 的,然而真有人卡这个吗?查询到同一个 \(mas\) 的概率都不小吧,何况类似的假复杂度又不少

严谨点,我们认为出题人一定会卡(预知了你的块长!!),那么就更改 \(B\)\(n^\frac23\) 然后暴力修改就解决了呢,简直是比摆烂的方法更加容易……总复杂度是 \(O(n^\frac 53)\),和一般的带修莫队一样。

代码写了后一个的,可以通过【模板】带修莫队,这个东西常数巨大(的确可以缩小但是还是会比莫队大)而且我懒得卡了(比如选择性挪左或者右边离得更近的指针之类的),但是保证正确。

讲解放代码里了,反正总共没两句话。

code

// code by 樓影沫瞬_Hz17
#include <bits/stdc++.h>

constexpr int N = 133333 + 10;

constexpr int B = 1050, T = N / B + 100, V = 1000000 + 10;

struct Moduis {
	int pos;
	uint16_t cnt[V];
} md[T];
int mas[T][T];
int cnt[V], a[N];
int lp[N], rp[N];
int n, m;

int main() {
	#ifndef ONLINE_JUDGE
		freopen("in.ru", "r", stdin);
		freopen("out.ru", "w", stdout);
	#endif
	using std::cin; using std::cout;
	std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i ++) cin >> a[i];
	for(int i = 1; md[i - 1].pos != n; i ++) {
		int L = B * (i - 1) + 1;
		md[i].pos = std::min(n, B * i);
		for(int j = L; j <= md[i].pos; j ++) lp[j] = i - 1, rp[j] = i; 
		for(int j = 1; j <= md[i].pos; j ++) ++ md[i].cnt[a[j]];
	}
	for(int i = 1; md[i - 1].pos != n; i ++) {
		int ans = 0;
		for(int j = i + 1; md[j - 1].pos != n; j ++) {
			for(int k = md[j - 1].pos + 1; k <= md[j].pos; k ++) 
				if(++ cnt[a[k]] == 1) ans ++;
			mas[i][j] = ans;
		}
		for(int j = md[i].pos; j <= n; j ++) cnt[a[j]] = 0;
	}
	char op;
	for(int c = 1, ans = 0, L, R, l, r, p, v; c <= m; c ++) {
		cin >> op;
		if(op == 'Q') {
			cin >> L >> R;
			if(rp[L] == rp[R]) {
				ans = 0;
				for(int i = L; i <= R; i ++) cnt[a[i]] = 0;
				for(int i = L; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++;
			} 
			else {
				int lk = rp[L], rk = lp[R];
				l = md[lk].pos, r = md[rk].pos + 1;
				ans = mas[lk][rk];
				for(int i = L; i <= l; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]];
				for(int i = r; i <= R; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]];
				for(int i = L; i <= l; i ++) if(++ cnt[a[i]] == 1) ans ++;
				for(int i = r; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++;
			}
			cout << ans << '\n';
		}
		else { // 就多了这里,是简单暴力修改
			cin >> p >> v;
			if(a[p] == v) continue;
			for(int i = 1; md[i - 1].pos != n; i ++) 
				if(md[i].pos >= p) md[i].cnt[a[p]] --, md[i].cnt[v] ++; // 修改数量
			for(int i = 1; md[i - 1].pos != n; i ++)
				for(int j = i + 1; md[j - 1].pos != n; j ++)
					if(md[i].pos + 1 <= p and p <= md[j].pos) { 
						if(md[j].cnt[v] - md[i].cnt[v] == 1) mas[i][j] ++;
						if(md[j].cnt[a[p]] - md[i].cnt[a[p]] == 0) mas[i][j] --; // 修改 mas
					}
			a[p] = v;
		}
	}
}
// 星間~ 干渉~ 融解~ 輪迴~ 邂逅~ 再生~ ララバイ~

在线莫队和回滚莫队之间不得不说的二三事

首先,在线莫队是可以有一个挺有用的常数优化的,也就是,在获得上文代码的 \(l,r\) 时,选择离着 \(L,R\) 更近的来做。

我是没写啦,在这里提一嘴是因为这对应着普通莫队,而我上面写的东西……没错,对应的是不删除莫队,也就是回滚莫队。

这难道不是十分自然的不删除吗?而且我感觉代码难度和回滚莫队差不多来着的。

而且很多回滚莫队的题都会稍微多给一点点的时间,导致如果空间允许其实这个一般不会被卡死,所以结果就是回滚莫队基本上它都可以写写。

posted @ 2025-11-11 22:06  樓影沫瞬_Hz17  阅读(18)  评论(1)    收藏  举报