没有引入就是最好的引入
——鲁迅 \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad ——鲁迅 ——鲁迅
为了骗分, O I e r OIer OIer们研发出了 O ( n n ) \mathcal{O(n \sqrt n)} O(nn)的算法——分块1(我不会说是我想不出来引入
分块 —— —— ——优雅的暴力
—— c q b z l h y \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad ——cqbzlhy ——cqbzlhy
在2009年的你面前有一道灰题2:
有一段连续的序列 a 1 a_1 a1 ~ a n a_n an,然后现在我们需要执行几类操作:
(一)1 l r
:求出 [ l , r ] [l, \;r] [l,r] 区间的和
你心里大喜,一看就是前缀和,记录 ∑ k = 1 i a k \sum_{k=1}^{i}{a_k} ∑k=1iak为 s u m i sum_i sumi,然后显然有 s u m i + 1 = s u m i + a i + 1 sum_{i+1}=sum_i+a_{i+1} sumi+1=sumi+ai+1,我们要求 ∑ i = l r a i \sum_{i=l}^{r}{a_i} ∑i=lrai就直接输出 s u m r − s u m l − 1 sum_r - sum_{l-1} sumr−suml−1呗,就一道黄题怎么会是灰题呢?
(二)2 l r x
:将 [ l , r ] [l, \; r] [l,r]区间加上 x x x
你微微一愣,但是你会一种叫线段树3的数据结构 ! ! 1 !!1 !!1, 完全可以水掉这道题.
(三)3 l r x
:在 [ l , r ] [l, \; r] [l,r]这个区间中,查询小于 x x x的前驱(比其小的最大元素)。
你暗暗吃惊,然后发现平衡树4可以水掉这道题.
数据范围:空间限制: 4 M B 4MB 4MB, 时间限制:1500 ms, 对于 100 % 100\% 100%的数据, 1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1≤n≤105(我们万恶的竞赛教练就是这么出的
你…倒在了电脑前,被一道小小的灰题打败了。
你不知道的是,一年后的莫涛队长(被尊称为莫队)发明了两种算法(莫队和分块,完美的解决了此类问题)
分块,顾名思义,就是把一段序列分成一小块一小块来处理,维护。
我们把一段当成一个整体,只记录维护整体的有关信息,就是分块。
首先,对于前言说得那道题,很朴素的做法就是:
1.从询问区间的l到r扫过去,每回加上扫到的值,即 a n s = ∑ i = l r a i ans=\sum_{i=l}^{r}{a_i} ans=∑i=lrai
2.直接把 a i a_i ai重新赋值不就得了 a i + = x ( l ≤ i ≤ r ) a_i += x(l \leq i \leq r) ai+=x(l≤i≤r);
3.从询问区间的 l l l到 r r r扫过去,每回遇到 < x
<x 的位置,答案记录最大值
代码:
while(q --){
int opt, l, r, x;
read(opt), read(l), read(r);
if(opt == 1){
int sum = 0;
for(int i = l; i <= r; i ++)sum += a[i];
write(sum);
putchar('\n');
}
else if(opt == 2)for(int i = l; i <= r; i ++)a[i] += x;
else{
int res = 0;
for(int i = l; i <= r; i ++)
if(a[i] < k)res = max(a[i], res);
write(res);
putchar('\n');
}
}
没错,这种做法很傻是不是?(我可能是真的傻子,这代码都写
但是,分块就是在这个基础上暴力优化的!!!
假设我们总共的序列长度为 n n n,然后我们把它切成 n \sqrt n n块,然后把每一块里的东西当成一个整体来看,
现在解释几个本文用到的术语:
然后我们先看看怎么得出答案:
1.对于完整的块5,我们希望有个东西能直接找出这整个块的和,于是每个块要维护这个块的所有元素的和。
2对于不完整块6,因为元素比较少(最多有 n n n / 块数 = n \sqrt n n个) 这时候当 n = 1000000 n=1000000 n=1000000的时候最多有 1000 1000 1000个,对比一下,我们可以直接暴力扫这个小块统计答案,
小技巧:如果这个不完整块被覆盖的长度>块维护的长度的一半,何不用这个块的和-没有被覆盖的元素的值呢?
2.这里,我们换种思路,记录一个lazy 标记(为什么用lazy,因为我很懒),表示整个块被加上过多少了,
.对于完整块,我们直接lazy+=加上的数x,块内的和ans+=x*元素个数(因为每个元素都被加上了x)
.对于不完整块,直接暴力修改就好了,顺便可以把lazy标记清了。
3.哎呀,这个有点难度啊,
.要在每个完整块内寻找小于一个值的元素数,
显然我们不得不要求块内元素是有序的,这样就能用二分(快速在一个有序的序列里查询的一个算法),对块内查询。
.不完整的块暴力就好
.这样的话需要提前对每块里面的元素做一遍排序就好.
.但是当有修改的话,因为整个块同时加上(减去)一个数,每个数的相对大小是不会变的,但是如果是不完全块就会改变,这样的话,还是因为元素个数小,重新新排一下不就得了?
然后,这道题就用了一种看似高大上的方法做完了……比之前傻傻的暴力是不是好看很多呢
其实这里的时间复杂度并不准确,只是大多数的代码近似于这个时间复杂度 ↩︎
在洛谷(一个 O I OI OI网站中)题目的难度分为灰, 红, 橙, 黄, 绿, 蓝, 紫, 黑, 灰。难度依次递增,而灰题是未判定难度的题。 ↩︎
一种树形结构,可以维护区间求和和单点修改的优秀数据结构 ↩︎
一种更加优秀的数据结构,可以完成大量的操作 ↩︎
完整块:被操作区间完全覆盖的块 ↩︎
不完整块:操作区间不完全覆盖的块 ↩︎