分块专题
调整块长的策略 (卡常技巧)
例题:
P5356
涉及 区间查询第 \(k\) 小 与 区间加 两种操作的多查询问题。
使用分块解决的思路:将每个块内的所有元素升序排序,并对每个块维护一个懒标记 \(lazy\),表示这个块内的所有元素已经增加了 \(lazy\);
对于区间加:
- 若某个块若所有元素都被操作,则仍然保持有序,无需重新排序,只需要在该块上的懒标记作增加;
- 而只有部分元素被操作的块(最多只有两个边界)需要暴力增加每个元素并对该块重新暴力排序。
对于区间查询:我们只需要知道对于任意一个子区间内 \(\leq v\) 的元素个数,外面套一层二分答案就行了。若当前要查询 \([l, r]\) 内 \(\leq v\) 的元素个数:
- 对于一个完整的块,其内部是有序的,我们只需要在块内二分就行了,还要注意先处理这个块的懒标记。
- 对于非完整块(两侧),直接暴力扫一遍就行,也需要注意处理懒标记。
具体实现见 code。
P3863
时间序列分块问题,代码就不补了,有问题看视频。
CF198E
利用分块对 bfs 过程剪枝,一道非常不错的题。
备忘:
- 每个磁铁都有质量 \(m\)、磁力 \(f\)、吸引半径 \(r\) 三种属性。
- 磁铁 \(a\) 被磁块 \(b\) 吸引的条件:\(m_{a}\leq f_{b} \space \wedge \space dist(a,b) \leq r_{b}\)
先想想暴力 bfs 是怎么做的:初始磁块先入队列,然后每弹出一个磁铁,将剩下未吸引过来的磁铁都 \(O(n)\) 检验一下是否可以用该磁铁吸引过来,最多有 \(n\) 个磁铁被吸引,那么总复杂度是 \(O(n^{2})\) 的。
我们考虑用分块优化内部的 \(O(n)\) 检验过程:
建立分块
将所有磁铁按照质量 \(m\) 升序排序,并记录每个块内所有磁铁的最大质量,然后分块(块长设置为 \(\sqrt n\))。将每个块内的所有磁铁按照与初始位置距离升序排序(重新排序后,块内部质量不再有序)
检验过程
每次检验的都是同一个块内的所有磁铁;若发现该块内的最大磁铁质量不超过当前吸引磁铁的磁力,则说明该块内的磁铁在质量上都能被吸引;那么我们就只需要考虑距离这个因素。而距离在每个块内部又是有序的,因此我们 直接从左向右扫一遍检验块内的每个磁铁(1) ,能被吸引的所有磁铁一定在该块内构成一个前缀。找到所有能被吸引的磁铁后,在块中删除掉这些磁铁(等价于修改这个块的左端点,\(O(1)\))。逐次检验每个块(2),直到该块内的磁铁最大质量超过了当前吸引磁铁的磁力,此时我们可以知道,在该块后的所有块的每个磁铁的质量都超过了当前吸引磁铁的磁力,无需再看了,因此我们 暴力看完当前块(3) 后就可以直接退出检验了。然后再进行后续 bfs 过程。
复杂度分析
仍然最多有 \(n\) 个点入队列,每个点都需要有检验过程,因此外层复杂度仍是 \(O(n)\);但对于内层,相当于上面标黑的 \(3\) 个子检验过程的复合。考虑每一项的复杂度:
- 虽然在 (1) 中对于每个块我们都要暴力看一遍,但是存在删除磁铁的过程,这样就可以保证每个磁铁最多只会被暴力看一次(以后更换吸引磁铁时就不会再被这一步检验了)。均摊下来是单独的 \(O(n)\) ,可以理解为常数。
- 共有 \(O(\sqrt n)\) 个块,因此这一步是 \(O(\sqrt n)\)。
- 块长是 \(O(\sqrt n)\) ,因此这一步也是 \(O(\sqrt n)\)。
于是,总复杂度是 \(O(n \sqrt n)\)。
具体实现见 code。
P5046
题意特别简单:给定一个排列,强制在线多次查询区间 \([l,r]\) 中的逆序对数量。
建立分块
将原排列按块长为 \(\sqrt n\) 分块,并将每个块内按 (值,下标) 二元组升序排序(实际上就是按值排序,但需要存原下标)。
需要预处理 5 种类型的信息:
- 函数 \(f(b1, l1, r1, b2, l2, r2)\):\([l1,r1],[l2,r2]\) 分别是块 \(b1,b2\) 内的区间,从 \([l1,l2]\) 中各取一个数,可形成逆序对的数量。
- \(pre_{i}\):位置 \(i\) 所在块的左端点 \(\backsim i\) 构成区间内的逆序对数量。
- \(suf_{i}\):\(i \backsim\) 位置 \(i\) 所在块的右端点 构成区间内的逆序对数量。
- \(g_{i,j}\):块 \(i \backsim\) 块 \(j\) 形成整个区间内的逆序对数量。
- \(cnt_{i,j}\):在前 \(i\) 个块中,\(\leq j\) 的值的数量。
查询处理
将所有查询 \([l,r]\) 分成 2 种类型:
-
\([l,r]\) 在某个块 \(b\) 内:
- \(l\) 恰好是这个块的左端点:\(pre_{r}\)
- 不是:\(pre_{r} - pre_{l-1} + f(b, bl_{b}, l-1, b, l, r)\)
-
\([l,r]\) 在跨多个块:需要再细分成 3 种类型的贡献:
- 只考虑左右两个散块(\(b1,b2\)):
- 只考虑中间的若干个整块 \((i \backsim j)\):
- 考虑两个散块与中间所有整块之间的贡献:可以暴力枚举散块中的数,利用 \(cnt\) 数组来计算。
复杂度分析:
- 预处理每张表的复杂度是 \(O(n\sqrt n)\) 的。
- 每调用一次 \(f\) 函数:由于每个块内元素是有序的,因此可以按照类似归并排序的方式用双指针线性计算,复杂度 \(O(\sqrt n)\)。
- 对于每一种类型的查询,要么是 \(O(1)\),要么是 \(O(\sqrt n)\)。
因此总时间复杂度就是 \(O(n \sqrt n)\)。
具体实现见 code。
树上分块
P3603
做法1:重剖分块
大致思路:按照树的 dfn 序将点权构成的序列分块,对于每个块都维护一个 bitset(长度为点权种类数 30000,每一位表示对应点权是否在该序列中存在);每次查询一个路径 \((x, y)\) 上的所有出现过的点权集合时,选择跳该路径上的重链,并每次查询该重链上的点权信息。由于每条重链上的 dfn 是连续的,因此可以通过分块(两个散块暴力扫并将相应位置 1;中间的整块直接将整个 bitset 异或到答案 bitset 上)做到 \(O(\sqrt n)\) 查询该重链上的 bitset,最后得到所有重链的 bitset ,再或运算到一起,就拼成了整条路径的点权集合 bitset。
复杂度分析:将每个询问当作是查询一条路径,那么每条路径会剖分成 \(O(\log n)\) 条重链,每条重链长度是 \(O(n)\) 的,需要用分块将其优化到 \(O(\sqrt n)\)(即 \(O(\sqrt n)\) 个整块的 bitset 或运算,散块的复杂度不计)。每次或运算复杂度是 \(O(\frac{A}{w})\)(\(A\) 为点权种类数),因此单次查询的复杂度是 \(O(\frac{\sqrt n * \log n * A}{w})\),总复杂度 \(O(\frac{m*\sqrt n * \log n * A}{w})\)。看着复杂度很高,但数据过水,可以过。
所以,树上分块其实就是在 dfn 序列上分块,不要想得很难。
做法2:随机撒点
先鸽着,以后碰到题了再去学~
CF1491H
序列分块结合树上分块。
题目看着也是特别吓人:给定一棵树 \(n\) 个结点,其中 \(1\) 是根节点,每个结点 \(i\) 的父亲 \(fa_{i} < i\)。\(m\) 条操作,两种类型:
- 给定常数 \(z\) 和区间 \([l,r]\),将 \(l \backsim r\) 上所有点的 \(fa_{u}\) 修改为 \(\max(1, fa_{u}-z)\)。
- 给定 \(x, y\),查询 \(lca(x, y)\)。
先作声明:本题和树上分块几乎没什么联系,只是看着像树上问题,做法本质上还是序列分块。
考虑暴力做法:只是单纯做好 \(fa\) 数组的修改工作,而在查询时,两个端点不断地暴力跳它们的 \(fa\),最终汇聚到的公共点就是它们的 \(lca\)。
修改和查询都是很慢的。那么,如果我们将所有结点按照编号大小排成序列并分块,对于每次修改 \(fa\),一个整块上的修改只是在上面打上懒标记(表示减量);而对于每次查询,我们都保证结点在向上跳的过程中一定出当前块(即一次性跳多次 \(fa\)),那么修改和查询的复杂度就都优化成了 \(O(\sqrt n)\)。
那么首先就有了一个问题:如何保证一个结点每次向上跳时都能出块?我们需要额外对每个结点 \(u\) 维护一个信息 \(out_{u}\),表示对于结点 \(u\),不断地跳它的 \(fa\),跳到的第一个出它所在块的结点。我们需要考虑同时维护维护 \(fa\) 和 \(out\) 这两个信息。
对于某个块 \(b\) 内的 \(out\) 初始化:
- 对于该块的左端点 \(l\),其 \(fa\) 一定已经出块了,因此 \(out_{l}=fa_{l}\)。
- 而对于其他块中结点 \(u\),若其 \(fa_{u}\) 已出块,则 \(out_{u}=fa_{u}\),否则 \(out_{u}=out[fa_{u}]\)。
对于修改,懒标记维护的仍然只是 \(fa\);对于 \(out\),我们在开始阶段只是暴力修改。但是,我们注意到,随着每一次修改,对于块中每个点的 \(fa\) 均至少减少 \(1\);那么当对某个块的修改次数达到了块长时,块内所有点的 \(fa\) 一定均已出块,也就是说,对于块内的 \(\forall\) 结点 \(u\),\(fa_{u}=out_{u}\)。那么此时我们就不再需要暴力维护 \(out\) 数组了,因为它们在后续是与 \(fa\) 取值相同的。
因此我们得到了关键结论:对于任意一个块,初始时我们采用暴力修改的方式修改其 \(out\) 数组(\(fa\) 开始时也暴力修改)。当对该块的暴力修改次数达到了该块的块长时,对于以后的修改,我们就只需要改 \(fa\) 的懒标记就行了,每个结点的 \(out_{u}\) 用 \(fa_{u}\) 代替。
复杂度分析
对于每个查询 \(lca\),我们每次都跳每个结点 \(u\) 的 \(out_{u}\),因此单次查询的复杂度是 \(O(\sqrt n)\) 的;而对于修改,每个散块只需要暴力修改 \(out_{u}\),复杂度 \(O(\sqrt n)\);而对每个整块,我们最多对其暴力修改 \(O(\sqrt n)\) 次,每次暴力修改 \(O(\sqrt n)\),因此每个块均摊下来是 \(O(n)\) 的,与修改了多少次无关。因此总时间复杂度就是 \(O(n \sqrt n)\)。
具体实现见 code。


浙公网安备 33010602011771号