数列分块九讲

本博客是数列分块入门 1~9 的题解,博主将会对九道题一一讲解,先给出链接:

  1. 数列分块入门 1
  2. 数列分块入门 2
  3. 数列分块入门 3
  4. 数列分块入门 4
  5. 数列分块入门 5
  6. 数列分块入门 6
  7. 数列分块入门 7
  8. 数列分块入门 8
  9. 数列分块入门 9

接下来进行讲解,包括思路和代码,且如果博主有想到非分块写法会一并给出,但没有代码。

约定序列长度为 \(n\) ,询问次数与 \(n\) 同阶,值域大小为 \(|V|\)

#6277. 数列分块入门 1

分块入门中的入门,要求支持区间加,单点查。

非分块做法显然,直接上差分树状数组就能解决,代码也很短,但既然是分块专项,还是试着分块解决一下。

分块这一数据结构的时间复杂度常常与根号挂钩,而和根号挂钩的算法常常有一个特点——优雅的暴力。

分块也脱不了关系,分块的思想常常是很暴力的,本题也是如此,我们可以将整个序列分成许多块,块长为 \(T\),则块数为 \(\dfrac nT\),对于整块,每次区间修改,我们可以考虑对于整块打一个与线段树的 \(\texttt{lazytag}\) 相似的标记,表示整个区间的变化量,这样的话我们对于修改区间中的整块,就可以做到 \(\mathcal O\left(1\right)\) 修改,对于区间两端不完整的块,最多只有两个,可以直接暴力修改,这样的话整块最多有 \(\mathcal O\left(\dfrac nT\right)\) 个,每个 \(\mathcal O\left(1\right)\),两端散块暴力修改时间复杂度 \(\mathcal O\left(T\right)\),算出来总时间复杂度是 \(\mathcal O\left(\dfrac nT+T\right)\),根据均值不等式,当 \(T=\sqrt n\) 时,时间复杂度最小值取到 \(\mathcal O\left(\sqrt n\right)\)

预处理就是把每个元素所属的块、块的左右端点标号,时间复杂度 \(\mathcal O\left(n\right)\)

查询直接自己的值加上区间的加标记就好了,时间复杂度 \(\mathcal O\left(1\right)\)

总时间复杂度 \(\mathcal O\left(n\sqrt n\right)\)

代码


#6278. 数列分块入门 2

本质上和上一题一样,但是查询变了,变成了查询比区间一个数小的数的个数,即查询区间排名。

非分块解法似乎可能能用线段树套可持久化 \(\texttt{FHQ-Treap}\) 或者 \(\texttt{Splay}\) 之类的,也可能不行,博主太菜了不懂,但是分块解法非常简单。

老套路,先分成 \(T\) 大小的 \(\dfrac nT\) 块(下文中无特色树说明默认同上,块长 \(T\),块数 \(\dfrac nT\)),再考虑整块和散块的修改和查询。

为了方便查询块内排名,可以想到在块内执行排序,这样就可以更快地块内查询。

发现这块打加法标记不影响块内排序,所以整块修改还是 \(\mathcal O\left(1\right)\) 的,而散块修改过一些以后,块内只有部分元素被修改了,所以需要重新排序,散块修改的时间复杂度是 \(\mathcal O\left(T\log T\right)\) ,总时间复杂度 \(\mathcal O\left(\dfrac nT+T\log T\right)\)

查询的时间复杂度,散块直接数,时间复杂度 \(\mathcal O\left(T\right)\) ,整块里面二分查找,查询总时间复杂度 \(\mathcal O\left(\dfrac nT\log T\right)\)

预处理比上面多了个预处理块内排序,时间复杂度 \(\mathcal O\left(n\log T\right)\)

这样看来总时间复杂度是 \(\mathcal O\left(n\left(T\log T+\dfrac nT\log T\right)\right)\)

还是当 \(T=\sqrt n\) 时时间复杂度最小取 \(\mathcal O\left(n\sqrt n\log\sqrt n\right)\)

代码


#6279. 数列分块入门 3

和上题一模一样,求前驱和求区间排名都是有序数组 \(\mathcal O\left(\log n\right)\) 做的事,直接和上题一样的做法,将查询做一点点修改就好了。

顺便提一嘴,这道题好像数据有锅,具体讨论区里有,大概就是体面里说会有负数,但实际上数据里面没有,就当作数据里没有负数做就好了。

代码


#6280. 数列分块入门 4

区间加区间和的经典题。

非分块算法显然,树状数组、线段树都可以做。

分块做法其实和第一题一模一样,无非再记一个区间和数组就完了,时间复杂度上查询变成和修改一样,别的不变,总时间复杂度还是 \(\mathcal O\left(n\sqrt n\right)\)

代码

#6281. 数列分块入门 5

前面都是数列分块模板题中的模板题,这道题开始需要一些思考了。

区间开方区间和,结论题。

首先讲结论,即一个数开方开不了几次就变成 \(1\) 了,\(x\) 的开方次数级别是 \(\mathcal O\left(\log\log x\right)\),证明如下:

设一个数为 \(v\),首先显然如果 \(2^k\ge v\),那么 \(\left\lfloor\sqrt[k]v\right\rfloor=1\),找到最小的 \(k=\left\lceil\log v\right\rceil\),然后每次平方等价于 \(k\gets2k\),这样的话平方操作的次数就是 \(\log k\)

综上所述,开方次数约是 \(\mathcal O\left(\log\log|V|\right)\)

得到了这个结论就可以做这道题了,查询操作同上一题不解释,修改操作散块依然直接做,整块记一个块内最大值,如果最大值为 \(1\) 显然这个区间可以跳过,因为每个数最多做 \(\mathcal O\left(\log\log v\right)\) 次,所以每个块最多做 \(\mathcal O\left(T\log\log|V|\right)\) 次就不用再做了,每次做完暴力重构这个块,修改的均摊总时间复杂度是 \(\mathcal O\left(nT\log\log v\right)\),另外询问复杂度还有 \(\mathcal O\left(\frac nT+T\right)\)

总时间复杂度 \(\mathcal O\left(nT\log\log|V|+n\left(\dfrac nT+T\right)\right)\),同样当 \(T=\sqrt{\frac n{\log\log|V|}}\) 时时间复杂度达到最优的 \(\mathcal O\left(n\sqrt{n\log\log|V|}\right)\)

代码


#6282. 数列分块入门 6

任意位置插入一个数、查询第 \(k\) 个数。

非分块做法一眼就是 \(\texttt{FHQ-Treap}\) 或者 \(\texttt{Splay}\) 模板题,随便一棵都行,实际上这题数据过于随机 vector 直接模拟就过了。

毕竟还是分块练习,考虑分块做法。

说说考虑一下,其实也就是把原数列分成几块,最开始每块里单次插入的时间复杂度是 \(\mathcal O\left(T\right)\) 的,但是如果故意来卡每次操作到同一个块内最后时间复杂度还是会退化成 \(\mathcal O\left(n\right)\) 的,但本题数据随机,期望下时间复杂度不会退化,所以可过。

查询更简单,先整块整块跳,当剩余排名比块内少时在块内找,时间复杂度 \(\mathcal O\left(\dfrac nT+T'\right)\),这里的 \(T'\) 表示修改到现在为止最大的块长,最大会变成 \(\mathcal O\left(n\right)\)

考虑如果不随机该怎样保证时间复杂度。

发现每次将分块重构时间复杂度是 \(\mathcal O\left(n\right)\) 的,所以如果重新分块次数不多,时间复杂度可以接受。

设初始块长 \(T=\sqrt n\),此时修改与查询时间复杂度都是 \(\mathcal O\left(\sqrt n\right)\)

考虑在多次修改后怎么保证时间复杂度。

加入我们每次设一个阈值,如果莫个块内的元素数增加量超过阈值就将所有块暴力重构,重构完后等于又回到了最开始的时候,也就是说每次重构结束后修改与查询时间复杂度还是 \(\mathcal O\left(\sqrt n\right)\)

设阈值为 \(T\) (此处的 \(T\) 并非块长,块长取 \(\sqrt n\) ),那么需要重构 \(\mathcal O\left(\dfrac nT\right)\) 次,重构的总时间复杂度是 \(\mathcal O\left(\dfrac{n^2}T\right)\),而最大查询与修改的时间复杂度为 \(\mathcal O\left(\sqrt n+T\right)\),即原有 \(\sqrt n\) 个元素,增加 \(T\) 个。

总时间复杂度 \(\mathcal O\left(\dfrac{n^2}T+n\left(\sqrt n+T\right)\right)\),当 \(T=\sqrt n\) 时时间复杂度最小稳定在 \(\mathcal O\left(n\sqrt n\right)\),这次时间复杂度就算想卡也不会退化了。

代码


#6283. 数列分块入门 7

又是板子题,和前面的数列分块入门 4 其实没有任何不同,无非多了一个区间乘,多打个标记就完了。

非分块做法依然是线段树。

有一点变化是块内既有加标记又有成标记,我们可以钦定先乘后加,这样的话如果在有加标记时来了乘操作,记得把加标记也乘一下,其他就没了。

代码


#6284. 数列分块入门 8

区间推平,区间查询。

非分块做法一眼题,要么线段树,要么直接上珂树,这道题就是道珂树模板题。

考虑分块做法,其实很简单,和前面的数列分块入门 5 有异曲同工之妙,但是这题更直接,不是开根几次,如果一个区间被整体推平了,之后的统计全都是 \(\mathcal O\left(1\right)\) 的,时间复杂度必然可以保证。

所以直接设块长 \(T=\sqrt n\),时间复杂度每次询问都只会把左右两遍的块暴力打散,也就是有一个 \(\mathcal O\left(\sqrt n\right)\) 的时间复杂度,其他的整块最多只会被暴力统计一遍,除非被打散,但打散最多只有 \(\mathcal O\left(n\right)\) 个块,所以时间复杂度是 \(\mathcal O\left(n\sqrt n\right)\) 的。

代码

顺便给一份珂树代码:

珂树代码

#6285. 数列分块入门 9

最终决战了,其实是双倍经验题。

洛谷 P4168 [Violet]蒲公英 和这题是一样的。

由于区间众数不具有许多优美的性质,所以类似树状数组或者线段树这种基于可加性的数据结构全部木大,还是得看分块,除了一个纯分块做法外还有一个阴间的回滚莫队套值域分块做法,比较阴间,不建议。

来讲讲分块做法。

首先考虑到暴力跑单词询问时间复杂度是 \(\mathcal O\left(n\right)\) 的,所以必挂。

考虑暴力的过程,每次多加一个数,就判断这一个数会不会成为当前的众数。

发现如果有一个区间的众数已经得到,那么区间内没有在区间外出现的数一定不构成众数。

所以可以考虑分块,把每一个块区间全都预处理好,这样的话整块查询的时间复杂度是 \(\mathcal O\left(1\right)\) 的,接下来是散块询问,需要快速询问区间内一个数的个数,主席树当然可以啦,但是不用那么麻烦,直接给每个数开一个数组,记录这个数出现的位置,二分查找区间内第一次出现和最后一次出现分别是序列中第几次出现,就可以在 \(\mathcal O\left(\log n\right)\) 时间复杂度内完成,单次询问的时间复杂度 \(\mathcal O\left(T\log n\right)\)

预处理部分直接从每个块的左端点暴力向后做,每碰到一个区间右端点就记录一下区间众数,时间复杂度 \(\mathcal O\left(\dfrac{n^2}T\right)\)

总时间复杂度 \(\mathcal O\left(nT\log n+\dfrac{n^2}T\right)\),当 \(T=\sqrt{\dfrac n{\log n}}\) 时时间复杂度最小取到 \(\mathcal O\left(n\sqrt{n\log n}\right)\)

代码


到此为止,数列分块入门就讲完了,从这些基础入门题中其实已经可以窥见分块强大处理能力的冰山一角可以说是数据结构之王,当然分块能做的当然不止这一点半点,但是做完这九道题确实可以让大家对分块有一个初步的认识,总体而言,这九道题都是蛮好的分块典型例题。

希望本博客让您有所收获,谢谢。

posted @ 2022-03-18 21:23  老莽莽穿一切  阅读(454)  评论(1编辑  收藏  举报