模型1:区间加法,单点询问
问题:给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,单点查值。
对每个块设置一个加法标记,记录这个块中元素一共加了多少,每次操作对整块直接 O(1) 标记,而不完整块元素较少,暴力修改元素的值即可,每次询问时返回元素的值加上其所在块的加法标记。
模型2:区间加法,询问区间内小于某个值 x 的元素个数
问题:给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的元素个数。
对于区间操作,其实只有三项东西需要思考:
- 要预处理的信息
- √n 个整块的处理
- 不完整块的 2√n 个元素的处理
考虑询问的情况:对于不完整的块的 2√n 个元素,直接枚举统计即可,而在整块内寻找小于某个值的元素,暴力枚举的时间复杂度过高,因此考虑使用二分查找,这就要求块内元素是有序的,因此需要在预处理时对每块做一遍排序,每次查询在 √n 个块内二分,然后暴力不完整块的 2√n 个元素即可
再考虑区间操作的情况:区间操作仍为区间加法,这样就可以套用 模型 1 的方法,维护一个加法标记,但不同的地方在于,不完整块修改后,可能会使得该块内数字乱序,所以头尾两个不完整块需要进行重新排序
这样一来,在加法标记下的询问操作,在块外暴力查询小于 (x-加法标记) 的元素个数,块内使用 (x-加法标记) 作为二分查找的值即可
模型3:区间加法,询问区间内小于某个值 x 的前驱
问题:给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的前驱。
所谓前驱,是指对于某个值 x ,比其小的最大元素
该模型可以延续 模型 2 的方法,将块内查询的二分进行修改,由于是要找的是 x 的前驱,因此暴力找左右不完整块的比 x 小最大值,再二分找整块比 x 小的最大值,取其中最大即可
模型4:区间加法,询问区间和
问题:给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间和。
相较于 模型 1 来说,该模型只是将单点询问改为了区间询问,不完整块仍是需要暴力,而要快速统计整块的答案,需要维护每个块的元素和,这就需要进行预处理。
在预处理时:求得每个块内的和,修改时对于边角料暴力修改值,修改了 a[i] 的值同时也要把 a[i] 所在块的值修改,对于中间的 k 块,修改标记 tag[i],表示第 i 个块加上了 tag[i],简单来说,即整块用一个 ans 数组来维护整块的和,不完整的块直接暴力加
在询问时:在 tag 里记下对于一个块一共加上了多少,然后对于边角料的处理与模型 1 类似,暴力+a[i]+tag[pos[i]],对于中间的 k 块,加上 ans[i],因为其中有 block 个数,因此要加 block 个 tag[i]
模型5:区间开方,询问区间和
问题:给出一个长为 n 的数列,数列中的元素最大为 2^31-1,以及 n 个操作,操作涉及区间开方,询问区间和。
由于在整块开方时,必须要知道每一个元素才能知道他们开方后的和,难以快速的对一个块进行信息更新,但不难发现,修改操作只有向下取整开方,而一个数经过几次开方后,其值最终会变成 1
因此,如果每次区间开方只涉及不完整块,那么仍然对 2√n 个元素直接暴力‘;如果涉及到整块,由于这些块经过若干次操作后都变为 1,此时可采用一种分块优化的暴力,即只要每个整块开方后,记录元素是否都变成了 1,这样在区间修改时,可以设置一个 flag 数组,用于标记块中元素是否全为 1,若是,则跳过那些全为 1 的块即可,若不是,则对块中元素进行暴力,由于数列中的元素最大为 2^31-1,因此最多开方 5 次后就为 1,时间复杂度在允许范围之内
模型6:单点插入,单点询问
问题:给出一个长为 n 的数列,操作涉及单点插入,单点询问,数据随机生成。
可以在块内维护数组以为的数据结构使其更具有拓展性,比如在每个块内存放 vector、set、multiset 等数据结构 ,这样如果还有插入、删除元素的操作,会更加的方便。
对于本模型来说,数据是随机生成的,可以在每块内维护一个 vector,每次插入时找到位置所在的块,再暴力插入,将块内其他元素直接向后移动一位,这样来实现插入操作,时间复杂度不会太高。
但对于数据不是随机的情况,如果在一个块内有大量的单点插入,会使得这个块的大小超过 √n,这样对块内暴力的复杂度就没有保证了,此时需要引入一个新的操作:重构,即重新分块。
重构,是每 √n 次插入后,重新将数列进行分块,由于重构所需的时间复杂度为 O(n),重构次数为 √n,因此重构的时间复杂度没有问题,且保证了每个块的大小相对均衡。
模型7:区间乘法与加法,单点询问
问题:给出一个长为 n 的数列,操作涉及区间乘法,区间加法,单点询问。
显然,如果只有区间加法或区间乘法,那么与 模型 1 没有本质区别,但当两者同时出现,需要考虑如何同时维护两种标记。
对于整块来说,由于加法操作会影响乘法操作的值,因此令乘法标记的优先级高于加法标记的优先级,则有:
- 若当前的一个块乘以 m1 后加上 a1,此时再进行一个乘以 m2 的操作,那么原来的标记变为:m1*m2、a1*m2
- 若当前的一个块乘以 m1 后加上 a1,此时再进行一个加上 a2 的操作,那么原来的标记变为:m1,a1+a2
因此,可以用 addTag[i] 来存放第 i 个整块的加数,mulTag[i] 存放第 i 个整块的系数,当一个整块进行乘法操作乘以 x 时,两个标记都要乘以 x,即:addTag[i]=addTag[i]*x,mulTaf[i]=mulTag[i]*x
对于不完整块来说,由于前面的操作可能将其当作完整块处理,因此每次不完整块都要更新为最新的值后再进行暴力
模型8:询问区间某值个数,区间赋值
问题:给出一个长为 n 的数列,操作涉及区间询问等于一个数 x 的元素,并将这个区间的所有元素改为 x。
区间赋值没有什么难度,但区间查询比较难,因为权值种类比较多,似乎没有什么好的维护方法
但通过模拟一些数据可以发现,询问后一整段区间都会被修改,几次询问后数列可能就只剩下若干个不同的区间了
考虑使用 模型 5 的方法,采用分块优化的暴力,建立一个标志数组,在维护每个整块的时候,判断该块是否只有一种权值,这样在区间操作的时候,对于同权值的一个块能在 O(1) 即可统计答案,若不是只有一种权值,那么就暴力统计答案,然后修改标记,而对于不完整块,同样使用暴力维护
这样一来,最差的情况每次都会消耗 O(n) 的时间,但实际上,可以这样进行分析:假设初始序列都是同一个值,那么查询是 O(√n),如果此时进行一个区间操作,其最多破坏首尾两个块的标记,因此只能使后面的询问至多多两个块的暴力时间,因此均摊后每次操作的复杂度仍为 O(√n),简单来说,要想让一个操作花费 O(n) 的时间,就要先花费 √n 个操作对数列进行修改
模型9:询问区间众数
问题:给出一个长为 n 的数列,以及 n 个操作,操作涉及询问区间的最小众数。
分块算法的优越性之一在于能询问区间众数,这是线段树解决不了的问题
对于 n 个数的序列,首先进行分块,预处理两块之间的众数,将两块间的最小众数记录下来,然后再进行查询
在每次查询时,首先将中间的整块的众数记录下来,然后再依次处理左边角料和右边角料,二分寻找是否能够更新,最后处理完成后,返回寻找到的值即可
值得注意的是,在进行分块时,有时题会卡常数,即将块分为 √n 时仍会 TLE,此时需要用两块分块大小不同的代码对拍,根据运行时间来不断调整分块大小,以减少常数