分块莫队学习笔记

优雅的暴力。

引入

link

这道题显然可以用线段树、树状数组做,但如果我偏不用这些数据结构呢?

我们知道,暴力修改和查询最坏是 O(n) 的,这样肯定会挂掉。

那该怎么办呢?

正题

分块

考虑将序列分成若干块,我们设每块长为 B

对于每次查询 [l,r],我们涉及到修改的块是 [bl,br]bi 代表 i 属于哪个块)。

其中 [bl+1,br1] 是整块都被修改了。

不妨设置一个懒标记,把每块的整块操作都加到这里面。

这样修改的复杂度是 O(nB) 的。

那剩下的我们就可以暴力操作,复杂度是 O(B) 的。

查询同理。

此时修改查询的复杂度就变成了 O(B+nB) 了。

使得该数最小的显然是 B=n,所以该算法的时间复杂度是 O(mn)

分块主要解决区修区查类问题,只要满足以下条件即可:

  • 可以打懒标记(结合律)。
  • 时间复杂度允许。

优势:可解决问题范围广。

劣势:时间复杂度高。

时间复杂度:O(mn)

空间复杂度:O(n)

莫队

普通莫队

莫队是一种离线算法,需要满足以下条件:

  • 在知道 [l,r] 的答案的情况下,可以 O(1) 求出 [l,r+1][l,r1][l+1,r][l1,r] 的答案。
  • 允许离线。
  • 只有询问没有修改。

首先将所有的询问离线下来,记为 [ql1,qr1],[ql2,qr2],,[qlm,qrm]

将询问排序(这正是莫队算法的精髓),从上一个询问的答案一个个改到当前询问,得到答案。

实现:

for (int i = 1; i <= m; i++) {
	while (l < q[i].l) del(l++);
	while (r > q[i].r) del(r--);
	while (l > q[i].l) add(--l);
	while (r < q[i].r) add(++r);
	ans[q[i].id] = res;
}

但是仔细分析发现时间复杂度仍然可以被卡成 nm,一点都不优秀,甚至会更慢。

考虑优化

我们想要优化复杂度的根本是让 lr 指针移动的距离尽量少。

对询问范围进行分块,块长为 B

以询问左端点的块编号为第一关键字,右端点为第二关键字排序。

  • 如果当前询问与上一次处于同一块,则 l 最多移动 B
  • 不同块的询问,l 最多移动 2B

则:

  • l 移动的复杂度是 m×B=mB
  • r 的复杂度是 nB×n=n2B

则复杂度是 O(mB+n2B)

使得该式最小的 B 的值是 nm,则此时的时间复杂度就是 O(nm+mlogm)

mlogm 是排序的复杂度。

总结一下。

普通莫队解决的问题满足以下条件:

  • 在知道 [l,r] 的答案的情况下,可以 O(1) 求出 [l,r+1][l,r1][l+1,r][l1,r] 的答案。
  • 允许离线。
  • 只有询问没有修改。

优势:再没有更快的思维做法之前,她几乎是跑得最快并且思维含量最低的。

劣势:只支持离线。

时间复杂度: O(nm+mlogm)

空间复杂度: O(n)

例题 1:小 B 的询问

非常板子的一道,维护一下 c 数组即可。

#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define FRE(x) freopen(x ".in", "r", stdin), freopen(x ".out", "w", stdout)
#define ALL(x) x.begin(), x.end()
using namespace std;

int _test_ = 1;

const int N = 50008;

int n, m, k, block_size, res, cnt[N], a[N], ans[N];
struct node {
	int l, r, id;
} q[N];

bool operator<(node x, node y) {
	int xl = (x.l - 1) / block_size + 1, xr = (x.r - 1) / block_size + 1;
	int yl = (y.l - 1) / block_size + 1, yr = (y.r - 1) / block_size + 1;
	return (xl != yl) ? (xl < yl) : (x.r < y.r);
}

void add(int x) {
	res += cnt[a[x]] * 2 + 1;
	cnt[a[x]]++;
}

void del(int x) {
	res -= cnt[a[x]] * 2 - 1;
	cnt[a[x]]--;
}

void init() {}

void clear() {}

void solve() {
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	block_size = n / sqrt(m); // 块长
	for (int i = 1; i <= m; i++) {
		cin >> q[i].l >> q[i].r;
		q[i].id = i;
	}
	sort(q + 1, q + m + 1);
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		while (l < q[i].l) del(l++);
		while (r > q[i].r) del(r--);
		while (l > q[i].l) add(--l);
		while (r < q[i].r) add(++r);
		ans[q[i].id] = res;
	}
	for (int i = 1; i <= m; i++) {
		cout << ans[i] << "\n";
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
//	cin >> _test_;
	init();
	while (_test_--) {
		clear();
		solve();
	}
	return 0;
}

不过此题块长就是 1 都能在 700 毫秒以内过,数据太水。

例题 2:小 Z 的袜子

也是非常板子的一道,维护一下 c 数组,并将上一题中的答案分别记分子分母即可。

请注意分子为 0 的情况。

#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define FRE(x) freopen(x ".in", "r", stdin), freopen(x ".out", "w", stdout)
#define ALL(x) x.begin(), x.end()
using namespace std;

int _test_ = 1;

const int N = 500008;

int n, m, k, block_size, len;
pii res;
int cnt[N], a[N];
pii ans[N];
struct node {
	int l, r, id;
} q[N];

bool operator<(node x, node y) {
	int xl = (x.l - 1) / block_size + 1, xr = (x.r - 1) / block_size + 1;
	int yl = (y.l - 1) / block_size + 1, yr = (y.r - 1) / block_size + 1;
	return (xl != yl) ? (xl < yl) : (x.r < y.r);
}

void add(int x) {
	res.first += cnt[a[x]];
	res.second += len;
	len++;
	cnt[a[x]]++;
}

void del(int x) {
	len--;
	cnt[a[x]]--;
	res.first -= cnt[a[x]];
	res.second -= len;
}

void init() {}

void clear() {}

void solve() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	block_size = n / sqrt(m);
	for (int i = 1; i <= m; i++) {
		cin >> q[i].l >> q[i].r;
		q[i].id = i;
	}
	sort(q + 1, q + m + 1);
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		if (q[i].l == q[i].r) ans[q[i].id] = {0, 1};
		while (l < q[i].l) del(l++);
		while (r > q[i].r) del(r--);
		while (l > q[i].l) add(--l);
		while (r < q[i].r) add(++r);
		if (res.first == 0) {
			ans[q[i].id] = {0, 1};
			continue;
		}
		int g = __gcd(res.first, res.second);
		ans[q[i].id] = {res.first / g, res.second / g};
	}
	for (int i = 1; i <= m; i++) {
		cout << ans[i].first << "/" << ans[i].second << "\n";
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
//	cin >> _test_;
	init();
	while (_test_--) {
		clear();
		solve();
	}
	return 0;
}

事实证明,还是 B=nm 跑得最快。

带修莫队

由于不能带修改实在是太别扭了,所以出现了带修莫队

带修莫队的思想跟所有可持久化数据结构是差不多的。

link.

由于加进了修改,我们无法再像正常莫队一样转移了。

可以考虑在迭代时增加一维时间戳。

每次就按顺序一个一个增加或减少修改即可。

同时就要以右端点所在块编号为第二关键字、时间为第三关键字排序。

时间复杂度与最优块长

设块长为 B、序列长度为 n、询问次数为 q、修改次数为 c

  • 左右端点移动上文分析过,是 qB+n2B 的。
  • 时间指针,对于每一个块,我们至多移动 c 次,即 nB×nB×c=cn2B2

总时间复杂度为 O(qB+n2B+cn2B2)

最优块长大概是……

n231/3(9m3n2+327m6n4m3n6)1/3+(9m3n2+327m6n4m3n6)1/332/3m

所以还是取一个更好看一点的。

譬如 B=n23

所以此时时间复杂度是约 O(n53)


总结一下,带修莫队需要满足以下条件:

  • 在知道 [l,r] 的答案的情况下,可以 O(1) 求出 [l,r+1][l,r1][l+1,r][l1,r] 的答案。
  • 允许离线。

优势:可以允许修改。

劣势:比思维方法慢且只能离线。、

时间复杂度:O(nlogn+n53)​。

空间复杂度: O(n)

例题1:数颜色 / 维护队列

按上文中写的模拟即可。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define FRE(x) freopen(x ".in", "r", stdin), freopen(x ".out", "w", stdout)
#define ALL(x) x.begin(), x.end()
using namespace std;

int _test_ = 1;

const int N = 2e6 + 5; 

int n, m, block_size, cnt_c, cnt_q, a[N], bel[N], cnt[N], ans[N], res;
struct query {
	int l, r, t, id;
} c[N], q[N];
bool operator<(query x, query y) {
	return (bel[x.l] != bel[y.l]) ? (x.l < y.l) : ((bel[x.r] != bel[y.r]) ? (x.r < y.r) : (x.t < y.t));
}
void build() {
	block_size = pow(n, 0.666);
	for (int i = 1; i <= n; i++) {
		bel[i] = (i - 1) / block_size + 1;
	}
}
void add(int x) {
	res += (cnt[x] == 0);
	cnt[x]++;
}
void del(int x) {
	cnt[x]--;
	res -= (cnt[x] == 0);
}
void upt(int x, int y) {
	if (q[y].l <= c[x].l && c[x].l <= q[y].r) {
		del(a[c[x].l]);
		add(c[x].r);
	}
	swap(a[c[x].l], c[x].r);
}

void init() {}

void clear() {}

void solve() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build();
	for (int i = 1; i <= m; i++) {
		char op;
		int l, r;
		cin >> op >> l >> r;
		if (op == 'Q') q[++cnt_q] = {l, r, cnt_c, cnt_q};
		else c[++cnt_c] = {l, r, 0, 0};
	}
	sort(q + 1, q + cnt_q + 1);
	int l = 1, r = 0, t = 0;
	for (int i = 1; i <= cnt_q; i++) {
		while (l > q[i].l) add(a[--l]);
		while (r < q[i].r) add(a[++r]);
		while (l < q[i].l) del(a[l++]);
		while (r > q[i].r) del(a[r--]);
		while (t < q[i].t) upt(++t, i);
		while (t > q[i].t) upt(t--, i); 
		ans[q[i].id] = res;
	}
	for (int i = 1; i <= cnt_q; i++) cout << ans[i] << "\n";
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  // cin >> _test_;
  init();
  while (_test_--) {
    clear();
    solve();
	}
  return 0;
}
posted @   Archippus  阅读(188)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示