「笔记」浅谈分块思想在一类数据处理问题中的应用笔记
前言
本文是对2013国家集训队论文中罗剑桥的论文——「浅谈分块思想在一类数据处理问题中的应用」的一些摘抄和整理。
引言
分块的核心思想
分块思想和传统的数据结构一样将数据有序化和层次化,但是方式有所不同。
核心思想:将一个集合划分成若干个规模较小的子集。
分块的良好性质
若子集规模很小,对每个子集可使用关于子集规模的复杂度较高的算法。
若子集数目很少,可使用与整个集合规模有关的算法分别处理每个子集。
为了均衡以上两点,既不能让子集规模过大,也不能让子集数目过大。因此一般分块算法的复杂度都与 \(O(\sqrt n)\) 有关。
数据的分块化
线性序列的分块化
典型问题是频繁地统计一个连续的子序列中满足一定性质的元素的和。
此处的“和”表示答案能够直接或间接地分拆成不同部分的答案的和。
分块的方法
从序列的第一个元素起,每连续的 \(S\) 个元素组成一个块。若最终剩余的元素不足 \(S\) 个,令他们组成一个块。
- 设 \(N\) 为序列长度,则上述方法中块的数目不超过 \(\lfloor N/S \rfloor + 1\) 。
- 设一次询问操作查询区间中 \([L,R]\) 的信息。那么除了完整块内元素意外需要单独处理的元素不超过 \(2S\)。
根据以上两个性质,如果能够以块为单位维护块内元素的信息的和,令块的大小 \(S\) 取合适的值,对于任意区间,就能快速找到对应的完整块与剩余元素,然后合并得到整个区间的信息。
例题1
有一个 \(N\) 个数的序列,每个数都是整数,你需要执行 \(M\) 次操作,操作有两种类型:
- \(Add\ D_i\ X_i\) 从第一个数开始,每隔 \(D_i\) 隔位置的数增加 \(X_i\)。
- \(Query\ L_i\ R_i\) 查询区间 \([L_i,R_i]\) 的和。
\(1\le N,M \le 10^5\),任何时刻数列中每个数的绝对值不超过 \(10^9\)。
分析
显然不能直接模拟,考虑用分块过掉这道题,每个块记录块内所有元素的和。
若块的大小设为 \(O(\sqrt N)\),则每次查询的时间复杂度是 \(O(\sqrt N)\)
比较显然,查询区间和时整块的和已经预处理出直接加上,散块暴力。
但是修改操作的复杂度依然没有改变,此时还是要用到分块思想。注意到一个事实:
当且仅当 \(Add\) 操作中 \(D_i< \sqrt N\) 时,需要更新的元素数目超过 \(\sqrt N\) 个。
因此将 \(Add\) 操作分类:
- 当 \(D_i\ge \sqrt N\) 时,直接更新涉及的元素以及块的和,复杂度 \(O(\sqrt N)\)。
- 当 \(D_i< \sqrt N\) 时,只用数组记录每个 \(D_i\) 的 \(Add\) 操作的修改量的和,复杂度 \(O(1)\)。
但是发现记录的每个元素和每个块的和只是考虑了第一类的修改操作,在查询时还要枚举第二类的所有 \(D_i\),并计算每个 \(D_i\) 的修改量的和对答案的影响。由于第二类的 \(D_i\) 不超过 \(\sqrt N\) 个,所以一次查询的时间复杂度依然是 \(O(\sqrt N)\)。
因此就可以在 \(O((N+M)\sqrt N)\) 的时间内解决这道题。
树形结构的分块化
在树形结构中,相邻的点之间有很强的相关性,一个直观的想法是希望将树划分成若干个规模较小的连通块。
有根树的 \(DFS\) 序列
无根树可以将任意一个点作为根从而转化生成有根树。
\(DFS\) 序列的定义:维护一个序列,最初是空的,从根节点开始进行深度优先遍历。每遇到一个点进栈或出栈就把它加到序列的末尾,遍历结束后得到的序列称为 \(DFS\) 序列。
最后我们会得到一个含有 \(2N\) 个元素的 \(DFS\) 序列,树中的每个点对应 \(DFS\) 序列中的两个元素。
\(DFS\) 序列的良好性质:\(DFS\) 序列中相邻的两个元素的关系仅有三种:同一个点、父子关系、兄弟关系。最后一种关系成立当且仅当 \(DFS\) 时前一个点元素是出栈的点而后一个点是进栈的点。
据此还可以证明下面的一条结论: \(DFS\) 序列中的第 \(u\) 项到第 \(v\) 项的连续子序列恰好对应从第 \(u\) 项对应的点 \(D_u\) 到第 \(v\) 项对应的点 \(D_v\) 的一条路径。路径上的点除了 \(D_u\) 和 \(D_v\) 的 \(LCA\) 以外至少都在子序列中出现了一次。(没有说明不在路径上的点,所以无视即可)
然后就可以使用上一节介绍的分块方法将树的 \(DFS\) 序列分成 \(S\) 块,那么序列中每一块的所有元素对应的点以及它们的 \(LCA\) 就是树上的一个连通块。连通块的数目不超过 \(2N/S\)。
上述方法实现起来比较简单,也具有分块思想的一般性质,即每个块的规模并不大而且块的数目并不多,但是存在某些点出现在不只两个连通块中。
例题2
给出一个 \(N\) 个点的树和一个整数 \(K\),每条边有权值,计算对于每一个点 \(i\),其他 \(N-1\) 个点到点 \(i\) 的距离中第 \(k\) 小的值是多少。
\(1\le K < N\le 50000\),边的权值是绝对值小于 \(1000\) 的整数。
分析
我们将树按之前描述的方法分成若干个大小为 \(O(S)\) 的连通块。考虑利用相邻的点之间的相关性更快地计算出以一个连通块内的每个点为根时的答案。
设点 \(u\) 是不在当前连通块内的任意一个点。无论以块内的哪个点作为根,从点\(u\) 到根的路径.上经过的第一个块内的点是不变的。据此,将不在当前块内的所有点按到根的路径上经过的第一个块内的点(以下简称最近块内祖先)分类,最多分成 \(S\) 类。
引理:设点 \(u\) 和点 \(v\) 是不在当前连通块内且属于同一类的任意两个点。设点 \(p\) 是点 \(u\) 和点 \(v\) 的最近块内祖先。则以连通块内的任意一个点为根时,点 \(u\) 到根的距离不大于点 \(v\) 到根的距离当且仅当点 \(u\) 到点 \(p\) 的距离不大于点 \(v\) 到点 \(p\) 的距离。
将树遍历一遍,计算出每个不在块内的点到最近块内祖先的距离。对每一类的所有点,把它们按到最近块内祖先的距离从小到大排序。接下来,枚举连通块内的每个点作为根,分别计算答案。
引理:以连通块内的任意一个点 \(r_i\) 为根时,可以在\(O(S\log^2 N)\)的时间复杂度内算出其它点到点 \(r_i\) 距离中第 \(K\) 小的值。
具体的做法如下。设当前以点 \(r_i\) 为根。为计算其它点到根 \(r_i\) 的距离中第 \(k\) 小的值,我们使用二分答案的方法。
设二分的答案为 \(X\)。从点 \(r_i\) 开始遍历块内的每个点,计算它们到点 \(r_i\) 的距离,并统计其中不超过 \(X\) 的值有多少个。然后对块内的每个点 \(p\),设点 \(p\) 到根 \(r_i\) 的距离为 \(Dis_p\),在以点 \(p\) 为最近块内祖先的一类点(按到点 \(p\) 的距离排序)的有序表中二分查找不大于 \(X-Dis_p\)的最大的数的位置,从而计算出这一类不在块内的点中与根 \(r_i\) 的距离不超过 \(X\) 的点的数目。
设到点 \(r_i\) 的距离不超过 \(X\) 的点的数目等于 \(cnt\)。
-
若 \(cnt < K\),说明实际答案大于 \(X\);
-
否则说明实际答案不大于 \(X\)。
于是每次二分可以将实际答案的范围缩小一半,从而以 \(O(S\log^2N)\) 的时间复杂度算其它点到点 \(r_i\) 的距离中第 \(K\) 小的值。
考虑排序的时间复杂度和连通块内的点的数目,可以在 \(O(N\log N + S^2 log^2 N)\) 的时间复杂度内计算出一个连通块内的每个点的答案。