1304 字
7 分钟

OI 分块

2024-02-18
2024-02-19
浏览量 加载中...

何为分块#

分块,正如其名,将一个整区间分为若干小区间进行操作。分块拥有比线段树更强的泛用性,但是时间复杂度略输一筹;分块代码更加直观、减少理解难度,但是时间复杂度稍逊风骚;分块的代码比线段树更短,但是时间复杂度惜败后者……线段树所上下传递的操作计算必须满足结合律,区间平均数、方差还行,像计算区间众数、中位数这样的问题,线段树就只能被薄纱了……

考虑到树状数组理解难度较大、较难调试,一般都选用泛用性强、码量折中、效率及格、调试简便的分块算法求解。

如何分块#

不同于线段树的二分存储,分块所使用的块状数组本质上只是一个带有块起始下标和块末尾下标的普通数组,就像在一列数中间插上分割线分出区块。对于区间修改操作,只需要进行以下几步就可以:

  1. 判断左端点l和右端点r所在的区块
  2. 若两个端点在同一个区块内:暴力循环更新值
  3. 若两个端点不在同一个区块内:在l所在区块内,从l开始循环至该区块结束,暴力更新值,相应的从r开始,往回循环至该区块起始处,暴力更新值,最后将二者中间整块的区块打上更新标记

我们会发现,分块其实就是将完全暴力的操作拆分成部分暴力的操作和部分取巧的操作。尽管看起来还是在使用暴力算法,但实际上优化了不少东西,而优化的效率还得取决于每个区块的长度。那么我们如何选择区块的长度呢?

最坏情况下,指定的左右端点就是整个数组的左右端点。假设总长度为nn,一般采取的区块长度是n\sqrt n。这样一来,就有n2\sqrt n-2个整区块会被打上标记,而两头的区块(长度都为n\sqrt n)暴力修改的复杂度是O(2n)=O(n)\mathcal O(2\sqrt n)=\mathcal O(\sqrt n)。这也就意味着分块是一种根号算法(时间复杂度是根号级别的算法),尽管它在时间复杂度上比不过线段树的logn\log n。但是只要满足n1018n\leq10^{18}或者mn109m\sqrt n\leq10^9mm为询问个数),基本上就不会TLE(1s)。

既然已经知道了n\sqrt n的来历,我们对数组进行分块时就简单了。首先循环n\sqrt n次用以记录区块的起点和终点,但是注意如果数组长度不为完全平方数就需要将最后一个区块(编号为n\sqrt n的区块)的终点下标设为nn以免访问越界。开两个长度略大于n\sqrt n的数组sted分别记录每个区块的起点下标和终点下标;加上一个查询数组bel,用于查找第ii个数所属的区块编号;同时为了避免后期访问到最后一个区块时出现下标越界RE的情况,再开一个长度n\sqrt n的数组记录每个区块长度(也可以只开一个变量存,特判是否是最后一个区块即可)。接下来就可以循环遍历整个数组了:

这是求解区间和的一段代码,init()函数中,我们把每个区块的和存在sum对应下标处

#include <bits/stdc++.h>
#define N 1000010
#define SQN 1010
typedef long long ll;
int st[SQN], ed[SQN], size[SQN], bel[SQN];
ll a[N], tag[N], sum[N];
void init() {
int sq = (int) sqrt(n);
for (int i = 1; i <= sq; i++) {
st[i] = sq * (i - 1) + 1;
ed[i] = sq * i;
size[i] = ed[i] - st[i] + 1;
}
ed[sq] = n;
size[sq] = ed[sq] - st[sq] + 1;
for (int i = 1; i <= sq; i++) {
for (int j = st[i]; j <= ed[i]; j++) {
bel[j] = i;
sum[i] += a[j];
}
}
}

按照如上所述的修改原理:

void update(int l, int r, int x) {
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) a[i] += x, sum[bel[i]] += x;
} else {
for (int i = l; i <= ed[bel[l]]; i++) a[i] += x, sum[bel[l]] += x;
for (int i = st[bel[r]]; i <= r; i++) a[i] += x, sum[bel[r]] += x;
for (int i = bel[l] + 1; i < bel[r]; i++) tag[i] += x;
}
}

询问区间和的原理比较相似,同样是分lr所在区块进行讨论。只是当进行整体区块操作时,用与线段树相似的方法——将标记乘到总和中并累加即可。

ll askSum(int l, int r) {
ll res = 0;
if (bel[l] == bel[r]) {
for (int i = l; i <= r; i++) res += (a[i] + tag[bel[l]]);
} else {
for (int i = l; i <= ed[bel[l]]; i++) res += (a[i] + tag[bel[l]]);
for (int i = st[bel[r]]; i <= r; i++) res += (a[i] + tag[bel[r]]);
for (int i = bel[l] + 1; i < bel[r]; i++) res += (size[i] * tag[i] + sum[i]);
}
return res;
}

以上求区间和的代码可以提交到洛谷 P3372 线段树模板 一里。与线段树相比,分块的速度慢了大约40ms,在300ms左右,还是可以接受的。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
OI 分块
https://justpureh2o.cn/articles/114/
作者
JustPureH2O
发布于
2024-02-18
许可协议
CC BY-NC-SA 4.0
最后更新于 2024-02-19,距今已过 735 天

部分内容可能已过时

评论区

Profile Image of the Author
JustPureH2O
穷方圆平直之情,尽规矩准绳之用
公告
JustPureH2O 的博客现已正式迁移至 Astro!原 Hexo 网站将移至 https://hexo.justpureh2o.cn/
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
100
分类
12
标签
55
总字数
372,306
运行时长
0
最后活动
0 天前

目录