数列分块

Jun 24, 2023
blog CP

基本思想

考虑一些在序列上的区间操作,如区间加和区间求和。

直接暴力做单次操作复杂度是 \(\Theta(n)\),总时间复杂度 \(\Theta(qn)\).

如果将序列分成 \(\sqrt{n}\) 块,每块长度为 \(\sqrt{n}\),并且对每个块维护里面所有元素的和,即可在 \(\Theta(\sqrt{n})\) 的复杂度内进行单次操作。

具体实现如下:

  • 令块的数量为 \(num=\sqrt{n}\),单个块的长度为 \(siz=\lceil\frac{n}{num}\rceil\).
  • 记 \(bel_{i}\) 表示 \(i\) 号节点属于第几个块。
  • 可以记录一些题目需要用到的信息,如 \(sum_{i}\) 表示第 \(i\) 个块内所有元素的和。

数列分块入门 9 题

T1 区间修改, 单点查询

对每个块维护整块加的值,查询的时候加上即可。

时间复杂度 \(\Theta(n\sqrt{n})\)。

T2 区间修改, 查区间内小于某个数的个数

对每个块内的元素排序,区间修改时整块大小顺序不变,散块暴力排序,查询时二分即可。

时间复杂度 \(\Theta(n\sqrt{n}\log n)\)。

排序使用归并可以将复杂度优化到 \(\Theta(n\sqrt{n\log n})\)。

T3 区间修改, 区间查前驱

同样是排序后在块内二分,只不过是把求比他小的数的数量改成求比它小的数中最大的那个。

时间复杂度 \(\Theta(n\sqrt{n}\log n)\)。

排序使用归并可以将复杂度优化到 \(\Theta(n\sqrt{n\log n})\)。

T4 区间修改, 区间求和

维护每个块的和以及加的数,用类似 \(\rm T1\) 的方法维护。

时间复杂度 \(\Theta(n\sqrt{n})\)。

T5 区间开方, 区间求和

我们发现开方操作是没办法全局维护的,于是考虑寻找开方操作的性质。

观察到每个数被开方很少的次数以后就会变成 \(1\)(或者它本身就是 \(0\),但这部分显然没有贡献,可以直接删去),于是对每个块记录一下里面 \(1\) 的个数,如果全都是 \(1\),就不对它进行开方,否暴力开方即可。

求区间和还是维护一下每个块的和。

时间复杂度 \(\Theta(n\sqrt{n})\)。

T6 单点插入, 单点查询

我们发现暴力做的时候需要将所有数向后移一位,单次时间复杂度 \(\Theta(n)\)。

于是考虑分块,给每个块预留 \(\sqrt{n}\) 的剩余空间用来后移,记一下当前块的长度即可。查询的时候每访问一个块就把查询编号减去当前块的长度。不难发现这样做单次操作时间复杂度 \(\Theta(\sqrt{n}).\)

但是我们发现一个问题,如果插入全在一个块内的话复杂度很快就会变成 \(\Theta(n)\),于是考虑每进行插入操作 \(\sqrt{n}\) 次就重构分块即可。

时间复杂度 \(\Theta(n\sqrt{n})\)。

T7 区间修改(包含加和乘), 单点查询

考虑对每个块记 \(\rm add\) 和 \(\rm mul\),表示块内元素的真实值为 \(a\times \rm mul+add\),区间加的时候,给 \(\rm add\) 加上 \(k\) 即可,区间乘的时候,不仅需要给 \(\rm mul\) 乘上 \(k\),还需要给 \(\rm add\) 乘上 \(k.\)

时间复杂度 \(\Theta(n\sqrt{n})\)。

T8 区间查询数的个数并全部修改成这个数

我们发现如果询问区间小的话查询和修改都很容易,如果询问区间大的话会把很长一段都变成相同的数。如果记录一个块是否都是同一个数的话就很容易维护了。

不难发现这样做复杂度是 \(\Theta(n\sqrt{n})\) 的。

T9 询问区间最小众数

记 \(s_{i,j}\) 表示前 \(i\) 个块中 \(j\) 出现的次数,\(p_{i,j}\) 表示第 \(i\) 个块到第 \(j\) 个块的众数。

我们发现,对于一次询问,答案一定是整块的众数或散块的众数,这个只需要对比一下即可。

时间复杂度 \(\Theta(n\sqrt{n})\)。

经典例题

[TJOI2009] 开关

记 \(c_{i}\) 表示第 \(i\) 盏灯是否开着,\(sum_{i}\) 表示第 \(i\) 个块中有多少盏灯开着,\(d_{i}\) 表示第 \(i\) 个块的状态是否整体改变。

对于区间取反操作,零散操作直接修改 \(c_{i}\),并根据 \(c_{i}\) 和 \(d_{i}\) 决定 \(sum_{i}+1\) 或 \(-1\). 整块操作不用改变 \(c\),直接令 \(sum_{i}=R_{i}-L_{i}+1-sum_{i}\),并且将 \(d_{i}\) 取反。

对于区间求和操作,零散点求和直接加上 \(c_{i}\oplus d_{i}\),整块求和则加上 \(sum_{i}\)。

单次操作复杂度 \(\Theta(\sqrt{n})\),总时间复杂度 \(\Theta(q\sqrt{n})\)。

[LG3396] 哈希冲突

转化题意,设计一种数据结构,支持:

  • 单点修改
  • 求从某一点 \(pos(pos\leqslant d)\) 开始,以步长 \(d\) 遍历数列,求所经过的所有数之和。

考虑根号分治。

对于 \(d\geqslant \sqrt{n}\) 的询问,直接暴力查询。

对于 \(d \leqslant \sqrt{n}\) 的询问,记 \(sum_{i,j}\) 表示从 \(i(i\leqslant \sqrt{n})\) 开始,步长为 \(j(j\leqslant \sqrt{n})\) 的答案。预处理的时候直接枚举计算即可,时间复杂度 \(\Theta(n\sqrt{n})\)。

对于修改操作 \(a[pos]\leftarrow x\),先直接在原数列上进行修改,再考虑其对 \(sum\) 数组造成了什么影响。易知,对于步长为 \(d\) 的预处理数组,只会对 \(sum_{pos\%d,d}\) 造成影响,于是对每个 \(d(d\leqslant \sqrt{n})\) 修改一下即可,单次修改时间复杂度 \(\Theta(\sqrt{n})\)。

总时间复杂度 \(\Theta[(n+q)\sqrt{n}]\)。

[LG2801] 教主的魔法

考虑分块。

记原数列为 \(A\),将 \(A\) 复制一份记为 \(B\),将 \(B\) 分块,块内排序。

对于修改操作,整块直接打标记,散点修改完后再块内排序,这样完成操作后仍能保持分块数列的性质,单次操作复杂度 \(\Theta(\sqrt{n}\log\sqrt{n})\)。

对于查询操作,二分查找第 \(i\) 个块内有多少个元素不小于 \(k-add_{i}\),单次操作复杂度 \(\Theta(\sqrt{n}\log \sqrt{n})\)。

总时间复杂度 \(\Theta[(n+q\sqrt{n})\log n]\)。

考虑再对复杂度做一些优化,令块的大小为 \(s\),则修改操作若采用归并排序,单次操作复杂度为 \(\Theta(s+\frac{n}{s})\),同时单次查询复杂度为 \(\Theta(s+\frac{n}{s}\log s)\)。当询问次数为 \(q\) 时,总复杂度为 \(\Theta[q(s+\frac{n}{s}\log n)]\geqslant \Theta(q\sqrt{n\log n})\),取最小值当且仅当 \(s=\sqrt{n\log n}\)。

[国家集训队] 排队

分块 \(+\) 树状数组。

考虑交换两个数 \(a_{u}\) 和 \(a_{v}\) 的贡献:

  • 显然 \(u\) 左边的数和 \(v\) 右边的数没有贡献。
  • 对于 \(a_{u}\) 来说,贡献为 \(u\) 到 \(v\) 之间比它大的数的数量减去比它小的数的数量。
  • 对于 \(a_{v}\) 来说,贡献为 \(u\) 到 \(v\) 之间比它小的数的数量减去比它大的数的数量。

至于如何计算 \(u\) 到 \(v\) 之间比某个数大或小的值的数量,分块后散块直接暴力,整块建两颗树状数组即可。

令块大小为 \(siz\),则时间复杂度为 \(\Theta[nlogn+q(siz+\frac{n}{siz}logn)]\geqslant nlogn+q\sqrt{nlogn}\),取最小值当且仅当 \(siz=\sqrt{nlogn}\),此时块的数量 \(num=\sqrt{\frac{n}{logn}}\)。

[LG4168] Violet 蒲公英

考虑分块,记 \(s_{i,j}\) 表示前 \(i\) 个块中颜色为 \(j\) 的数量,\(p_{i,j}\) 表示第 \(i\) 个块到第 \(j\) 个块的众数。

对于一次询问,先求出整块中的众数、众数的出现次数和所有在零散块中出现了的颜色的数量。显然,如果零散块中出现的颜色没有一个比众数出现的次数多,那么要求的众数就是整块的众数,否则是出现次数最多的颜色。

时间复杂度 \(\Theta(n\sqrt{m})\)。此时块大小 \(B=\frac{n}{\sqrt{m}}\)。