学习笔记——数列分块
一、没什么用的前言
首先,让大家来看一张表格
| POJ3468 | 复杂度 | 时间 | 内存 | 代码 | 优劣 |
|---|---|---|---|---|---|
| 树状数组 | \(O((N+Q)logN)\) | \(1.0s\) | \(3MB\) | \(850B\) | 效率高、代码短 不易扩展、不太直观 |
| 线段树 | \(O((N+Q)logN)\) | \(1.5s\) | \(7MB\) | \(1700B\) | 效率较高、扩展性好 代码较长、直观性一般 |
| 分块 | \(O((N+Q)\sqrt{N})\) | \(1.9s\) | \(1.5MB\) | \(1500B\) | 通用、直观 效率较低、码长一般 |
| 朴素 | \(O((N+Q) \times N)\) | \(TLE\) | \(1MB\) | \(500B\) | 效率低 通用 |
--\(from\) 《算法竞赛进阶指南》
看到这里,\(OIer\)们作何感想?(我还学什么分块啊,学树状数组和线段树不香吗?),所以本篇博客到此结束。
好了好了,先收一下浮躁的内心,让我们换个角度再思考一遍:
我们学会了树状数组 (时空复杂度吊打其他数据结构) ,但那也只能解决单点修改区间查询的问题(加个差分可以解决区间修改单点查询),面对其他问题,我们不就束手无策了吗?
有\(OIer\)就要说了:那我用线段树不就可以了吗?
那如果问题不具有区间可加性呢?
这时候,就轮到分块闪亮登场了!
俗话说的好(其实只在\(OI\)界流行):复杂度越高的算法,能处理的问题就越多,功能就越全面。所以尽管分块的复杂度更高,但处理起问题来,应用的也更为广泛。
二、分块的概念
我们先回顾树状数组和线段树的原理:一个基于二进制划分与倍增,一个基于分治,它们都将序列分为几段,再对每一段进行维护。
同理,分块就是将序列分为几个小块,并采用整块维护,局部朴素的方法进行查询,最后得出结论。
最优分块公式:
\(blo\)(分块的块长)\(=\) \(max(1,\dfrac{n}{ \sqrt{(m \times log_2n) }})\)
其中\(n\)为数列长度,\(m\)为询问次数。
三、练习
是不是感觉很奇怪,分块的概念就这么点?没错,就这么点,但其题目的难度也够考验(e xin)我们了。
不信,那就先来一下九道入门题:
两篇不错的题解:
T1
思路:和线段树一样,用一个add数组记录整块加的数,再查询即可。
T2
思路:用另一个数组记录从小到大排好序后的整块内元素,再用lower_bound(返回第一个\(>=\)查找值的下标)统计个数,其他与\(T1\)一样。
T3
思路:和\(T2\)一样,先用另一个数组记录从小到大排好序后的整块内元素,再用lower_bound找整块内的前驱以及朴素扫描分块多出部分的前驱。
T4
思路:和线段树模板\(1\)一样,用add和sum两个数组记录整块的修改操作,再查询即可。
T5
思路:加一个标记数组,如果该整块内的元素都为\(0\)或\(1\)时,就标记该整块,之后不用再修改。
T6
思路:借助vector进行插入操作,当一个块的长度超过一定限度时,将整个数组进行分块的重构,防止因块长过长而\(TLE\)
T7
思路:和线段树模板\(2\)一样,用add和mul两个数组记录整块的修改操作,再查询即可。
T8
思路:加一个标记数组(设为tag),初始时全部赋值为\(INF\)。修改时对于整块直接修改标记,对于两端多出的部分先把整个块赋值为标记,把tag数组赋值为\(INF\),再修改两端多出的部分\(a[i]\)的值。查询时对整块直接判断标记,如果tag为\(INF\)就朴素扫描该块,对于两端多出的部分先下推tag再扫描即可。
T9
思路:本题为区间众数,不具有区间可加性,只能分块处理。
对于众数的来源,只可能为以下三种情况:
1.来源于l到pos[l]*blo(pos[l]表示\(l\)所属的分块)之间。
2.来源于pos[l]+1到pos[r]-1之间。
3.来源于(pos[r]-1)*blo+1到r之间。
我们可以用一个二维数组f[i][j]记录块\(i\)到块\(j\)之间的众数,并对原数组进行离散化,用vector记录\(a[i]\)中的元素的位置,用upper_bound减去lower_bound即可得到区间内该元素出现次数。
怎么样,感受到分块的妙处了吗?那我们再刷几道洛谷的分块题吧!
P2801 教主的魔法(类似入门二)(适合分块入门的题解)我的代码
P4145 上帝造题的七分钟2 / 花神游历各国(类似入门五)(分块题解(当然其他数据结构也能过,但咱们这儿是分块练习嘛))我的代码
P4168 (Violet)蒲公英(类似入门九)(题解)(我的代码)
P5356 [Ynoi2017] 由乃打扑克(题解)(我的代码)
众所周知,\(YNOI\)是分块卡常专项练习题,所以咱们的卡常技巧当然是少不了的:
-
每一次修改后,对于只修改了一部分的小段,因为修改段和未修改段都还是单调的,所以可以运用归并排序的方法\(O(n)\)排序,不必用\(sort\)花\(O(nlogn)\)的时间
-
查询时,对于多出的两个小段,我们可以用归并排序将其合并为一个块(原理同上),再进行二分答案
-
二分答案时,二分的边界为每一块的最大值的最大值,每一块的最小值的最小值,不用从\(-INF\)到\(INF\)。
-
大优化 :如果一次二分至于得到的答案(\(<=mid\)的数的个数)小于要求时,可以统计每一块中最后一个小于等于\(mid\)的数的位置,把该块的答案初始值设为该位置\(-\)该块左起点位置\(+1\),再把该块的左起点设为该位置\(+1\),可大量减少二分区域。
P5309 [Ynoi2011] 初始化(题解)(我的代码)
本题属于分块\(+\)根号分治,要对\(x\)的范围进行分类讨论
-
当\(x>blo\)时,因为要修改的区间小于\(\sqrt n\),所以直接暴力修改即可
-
当\(x<blo\)时,我们可以以\(x\)的值作为块长统计贡献:
因为笔者不太会使用画图软件,所以献上一张丑图将就着看吧

如上图所示,我们可以用一个二维数组记录该修改对红蓝各部分的贡献的前缀后缀和
我用pre[i][j]表示当块长为\(i\)时距离块首为\(j\)的位置对答案的贡献的前缀和,用suf[i][j]表示块长为\(i\)时距离块首为\(j\)的位置对答案的贡献的后缀和。
-
对于\(l\)、\(r\)在块长为\(i\)时的同一块的情况,只需要用\(r\)位置的贡献的前缀和减去\(l-1\)位置的贡献的前缀和即可
-
对于\(l\)、\(r\)在块长为\(i\)时的不同块的情况,用\(l\)位置的贡献的后缀和加上\(r\)位置贡献的前缀和,再加上\(l\)和\(r\)之间间隔的整块数\(\times\)该块的贡献(
pre[i][i]或suf[i][1])即可


浙公网安备 33010602011771号