【学习笔记】浅谈较为基础的分块

· · 算法·理论

Total

关于数列分块入门、数论分块的讲解。

分块是一种思想,因此其可以应用各个方面,例如对询问分块优化时间复杂度、实现块状数组这种数据结构等。

数列分块入门

来自 loj 的数列分块入门题目,是一些常见的区间问题的维护。

前言

数列分块是种优雅的暴力。

数列分块入门 1

区间加法,单点查询。

我们记块长为 B,考虑对数列进行分块,将元素分入不同的块中,记第 i 的元素属于 blk_i

其中如何计算:blk_i = \lfloor{\frac{i-1}{B}}\rfloor + 1

对于修改区间中零散的不成整块的点,我们直接暴力更新 a_i \leftarrow a_i + c

对于整块中的元素进行加法,我们可以给这样的整块记一个加法标记 tag_i,即我们最后查询点 x,查询出来的值应是 a_x + tag_{blk_x},意为现在这个位置的值加上它所属块应该加上的值。

这样就完成了 Code。

你发现我们还没有进行 B 的取值的讨论,所以 B 该取多少呢?

对于散块的处理大约是 O(B) 的,整块则是 O(\frac{n}{B}),总复杂度就是 O(n(B + \frac{n}{B})),这个式子在 B = \sqrt{n} 时取得最小值,复杂度就对应的是 O(n\sqrt{n}) 的。

数列分块入门 2

区间加法,询问区间内小于某个值 x 的元素个数。

设块长为 T,依旧是考虑散块暴力统计,整块考虑二分,但是显然我们需要时刻保持数组有序。

对于修改散块的情况,本质上是修改了某个整块的一部分,所以我们需要对其所属整块进行重新排序,复杂度 O(T\log T)

对于整块,依旧是打加法标记,我们一开始就将所有整块排序再打加法标记不会影响块内顺序,复杂度 O(\frac{n}{T}\log T)

这样看来总复杂度就是 O(n(T\log T + \frac{n}{T}\log T)),在 T = \sqrt{n} 时取得最小值,此时复杂度 O(n\sqrt{n}\cdot\log{n})

Code。

数列分块入门 3

区间加法,询问区间内小于某个值 x 的前驱(比其小的最大元素)。

同上一道题的理,依旧是有序数组可以处理的问题,不过是二分必须手写罢了

复杂度一样 O(n\sqrt{n}\cdot\log{n})

Code。

数列分块入门 4

区间加法区间求和,典中典。

用线段树、树状数组等是很好实现的,考虑分块怎么做。

实则是同理的,我们对每个块记一个块内和数组 sum_i,每次修改散块 a_i \leftarrow a_i + c 就同时 sum_{blk_i} \leftarrow sum_{blk_i} + c

整块同时更新加法标记 tag_isum_i 即可。

设块长为 T,复杂度 O(\frac{n}{T} + T),在 T = \sqrt{n} 最小为 O(n\sqrt{n})

Code。

数列分块入门 5

区间开方区间和,如果你做过这道题那应该是比较熟悉的。

对于一个数 v,实际上开平方不用多少次就可以使它变成 1,这个级别大概是 O(\log\log v) 的。

记块长为 T

这样一来我们考虑比较暴力的修改,对于散块每次 a_i \leftarrow \sqrt{a_i} 然后同步更新块内和 sum_{blk_i},复杂度 O(T\log\log \left|V\right|)

对于整块,如果说块内元素均小于等于 1,给该块打上标记下次就不处理了,复杂度 O(nT\log\log v)

加上询问就是 O(nT\log\log v + n(\frac{n}{T} + T)),在 T = \sqrt{\frac{n}{\log\log \left|V\right|}} 时最快。

Code。

数列分块入门 6

单点插入,单点询问,数据随机生成

既然你每次插入,你考虑每次炸掉当前块重构,这样的复杂度是 O(n) 的,如果次数不多是可以的。

一开始钦定块长为 \sqrt{n},修改查询的复杂度均为 O(\sqrt{n})

但是但凡是多次修改重构的复杂度会大大增加,所以我们考虑一个阈值 B,如果块内元素的个数超过阈值就炸掉重构。

这样一来重构复杂度是 O(\frac{n^2}{B}) 的,修改查询是 O(\sqrt{n} + B) 的,总的复杂度就是 O(\frac{n^2}{B} + n(\sqrt{n} + B)) 的,在 B = \sqrt{n} 是取到最小是 O(n\sqrt{n})

Code。

数列分块入门 7

区间乘法,区间加法,单点询问。

其实只需要对每个区间打乘法标记,钦定加法修改的时候先乘后加,乘法修改也要给整块的加法标记乘 c

复杂度块长取 \sqrt{n} 最小是 O(n\sqrt{n})

Code。

数列分块入门 8

区间询问等于一个数 c 的元素,然后区间覆盖为 c

用珂朵莉树做着题会很好做,本人不会。

其实分块也还好做,考虑给每个块打覆盖标记,修改整块的时候更新当前块的标记,修改散块的时候需要下放标记将其所在块都修改成标记,若当前散块没有标记是不用更新标记的,直接暴力修改 a_i \leftarrow c

查询的话边修改边统计就行,依旧是块长取 \sqrt{n} 时复杂度最小为 O(n\sqrt{n})

Code。

数列分块入门 9

查询区间最小众数。

考虑整块和散块的关联,其实整块的最小众数预处理出来就是散块的数会影响到整块。

设块长为 T

所以考虑预处理整块内的众数,记 num_{l,r} 表示块 l \sim r 的最小众数,直接枚举每个块右端点然后往 n 扫,这样复杂度是 O(\frac{n^2}{T})

之后记录每个数出现的位置,因为我们需要知道每个数区间内出现次数,可以用二分解决,先找到第一个大于询问右端点的位置,再找到第一个大于等于询问左端点的位置,两者相减就是想要的出现次数。

每次查询再用散块更新答案,复杂度就是 O(T\log n)

总复杂度 O(T\log n + \frac{n^2}{T}),在 T = \sqrt{\frac{n}{\log n}} 取得最小值 O(n\sqrt{n\log n})

Code。

数论分块

数论分块,又称整除分块,通常用于求解 \sum\limits_{x=1}^{n}{f(x)g(\lfloor\frac{n}{x}\rfloor)} 的形式,如果能够在 O(1) 的时间内算出 f(x),数论分块就能 O(\sqrt{n}) 内算出答案。

一些细节

假设 n \in \N_{+},同时我们有左右端点 l,r \in [1,n],l \leq r,若满足 \lfloor\frac{n}{l}\rfloor = \lfloor\frac{n}{r}\rfloor,那么满足条件的最大的 r = \lfloor\frac{n}{\lfloor\frac{n}{l}\rfloor}\rfloor

也就是是说事实上我们是给所求式子的值中某一段相同的值分成一段进行处理,比如一段中所有值都为 k = \lfloor\frac{n}{l}\rfloor,那么可以得到这个段最大的右端点为 \lfloor\frac{n}{k}\rfloor

为什么呢?

首先对于 x \in [l,r],你都要满足 \lfloor{\frac{n}{x}}\rfloor = k,也就意味着 n = xk + y(0 \leq y < x)

n = xk + y \\ \downarrow \\ n \geq xk \\ \downarrow \\ x \leq \frac{n}{k} ### 多维数论分块 同理,求的是形如 $\sum\limits_{i=1}^{n}{\lfloor\frac{x_1}{i}\rfloor\lfloor\frac{x_2}{i}\rfloor\dots\lfloor\frac{x_k}{i}\rfloor}$ 的式子的值。 这时候 $r = \min\limits_{j=1}^{k}{\lfloor{\frac{x_j}{\frac{x_j}{l}}}\rfloor}$。 ### Ex.1 [UVA11526 H(n)](https://www.luogu.com.cn/problem/UVA11526) 这是数论分块的模板,求 $\sum\limits_{i=1}^{n}{\lfloor\frac{n}{i}\rfloor}$。 枚举左端点 $l$,每次 $r = \lfloor{\frac{n}{\lfloor{\frac{n}{l}}\rfloor}}\rfloor$,那么 $l \sim r$ 内的所有数就都是 $\lfloor{\frac{n}{l}}\rfloor$,求和就加上 $(r - l + 1) \times \lfloor{\frac{n}{l}}\rfloor$ 即可,每次 $l \leftarrow r + 1$ 向后遍历。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int Test = 0, n = 0; inline void Solve() { scanf ("%lld", &n); int Sum = 0; for (int l = 1, r = 0; l <= n; l = r + 1) { r = n / (n / l); Sum += (r - l + 1) * (n / l); } return printf ("%lld\n", Sum), void(); } signed main() { scanf ("%lld", &Test); while (Test --) Solve(); return 0; } ``` ### Ex.2 [P2261 [CQOI2007] 余数求和](https://www.luogu.com.cn/problem/P2261) 考虑变换式子: $$ \begin{aligned} G(n,k) &= \sum\limits_{i=1}^{n}{k \bmod i} \\ &= \sum\limits_{i=1}^{n}{k - \lfloor{\frac{k}{i}}\rfloor \cdot i} \\ &= nk - \sum\limits_{i=1}^{n}{\lfloor{\frac{k}{i}}\rfloor \cdot i} \end{aligned} $$ 出现了 $\sum\limits_{i=1}^{n}\lfloor{\frac{k}{i}}\rfloor \cdot i$,考虑数论分块。 因为每块中 $\lfloor{\frac{k}{i}}\rfloor$ 相等,把它提出来 $\lfloor{\frac{k}{i}}\rfloor\sum\limits_{i=1}^{n} i$,后面直接求和公式 $\lfloor{\frac{k}{i}}\rfloor\frac{(l+r)\times(r-l+1)}{2}$,但是你注意不保证 $k > n$,也就是说 $\lfloor\frac{k}{l}\rfloor$ 可能为 $0$,此时右端点 $r$ 无意义,需要特判一下,此时 $r = n$ 即可。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int n = 0, k = 0, Sum = 0; signed main() { scanf ("%lld %lld", &n, &k); for (int l = 1, r = 0; l <= n; l = r + 1) { if (k / l) { r = min (n, k / (k / l)); } else { r = n; } Sum += (k / l) * (r - l + 1) * (l + r) / 2; } return printf ("%lld\n", n * k - Sum), 0; } ``` ### Ex.3 [P2260 [清华集训 2012] 模积和](https://www.luogu.com.cn/problem/P2260) 拆式子题。 $$ \sum\limits_{i=1}^{n}{\sum\limits_{j=1}^{m}{(n \bmod i)(m \bmod j)}} \\ \Rightarrow \sum\limits_{i=1}^{n}{(n \bmod i)} \cdot \sum\limits_{j=1}^{m}{(m \bmod j)} - \sum\limits_{i=1}^{n}{(n \bmod i)(m \bmod i)} \\ \Rightarrow \sum\limits_{i=1}^{n}{(n - \lfloor{\frac{n}{i}}\rfloor \cdot i)} \sum\limits_{j=1}^{m}(m - \lfloor{\frac{m}{j}}\rfloor \cdot j) - \sum\limits_{i=1}^{n}{(n - \lfloor{\frac{n}{i}}\rfloor \cdot i)(m - \lfloor{\frac{m}{j}}\rfloor \cdot j)} $$ 式子的前两项与上一题同理求,后面一项再拆: $$ \sum\limits_{i=1}^{n}{(n - \lfloor{\frac{n}{i}}\rfloor \cdot i)(m - \lfloor{\frac{m}{j}}\rfloor \cdot j)} \\ \Rightarrow \sum\limits_{i=1}^{n}{nm - n\lfloor\frac{m}{i}\rfloor \cdot i - m\lfloor{\frac{n}{i}}\rfloor i + \sum\limits_{i=1}^{n}{\lfloor{\frac{n}{i}}\rfloor\lfloor{\frac{m}{i}}\rfloor \cdot i^2}} $$ 最后一项正是上文提到的二维数论分块,同时有平方和公式:$\sum\limits_{i=1}^{n}{i^2} = \frac{n(n+1)(2n+1)}{6}$,这样就好求了,具体看代码。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; constexpr int MOD = 19940417, inv2 = 9970209, inv6 = 3323403; int n = 0, m = 0, As1 = 0, As2 = 0, res = 0, val1 = 0, val2 = 0, val3 = 0; inline int calc1 (int x) { return x * (x + 1) % MOD * inv2 % MOD; } inline int calc2 (int x) { return x * (x + 1) % MOD * (2 * x + 1) % MOD * inv6 % MOD; } signed main() { scanf ("%lld %lld", &n, &m); if (n > m) swap (n, m); As1 = n * n % MOD, As2 = m * m % MOD; for (int l = 1, r = 0; l <= n; l = r + 1) r = n / (n / l), As1 = (As1 - (calc1 (r) - calc1 (l - 1) + MOD) % MOD * (n / l) % MOD + MOD) % MOD; // 式子第一项 for (int l = 1, r = 0; l <= m; l = r + 1) r = m / (m / l), As2 = (As2 - (calc1 (r) - calc1 (l - 1) + MOD) % MOD * (m / l) % MOD + MOD) % MOD; // 式子第二项 for (int l = 1, r = 0; l <= n; l = r + 1) { val1 = val2 = val3 = 0, r = min (n / (n / l), m / (m / l)); // 多维数论分块取最小的右端点 val1 = (val1 + (r - l + 1) * n % MOD * m % MOD) % MOD; val2 = (val2 + (calc1 (r) - calc1 (l - 1) + MOD) % MOD * ((m / l) * n % MOD + (n / l) * m % MOD) % MOD) % MOD; val3 = (val3 + (calc2 (r) - calc2 (l - 1) + MOD) % MOD * (n / l) % MOD * (m / l) % MOD) % MOD; res = (res + val1 - val2 + val3 + MOD) % MOD; } return printf ("%lld\n", (As1 * As2 % MOD - res + MOD) % MOD), 0; } ``` ## 后记 附赠题单: [数论分块](https://www.luogu.com.cn/training/249197#information)。 [进阶分块题单](https://www.luogu.com.cn/training/11849)。