数论分块可以快速计算一些形如
的和式.如果可以在 时间内计算出 或已经预处理出 的前缀和时,数论分块就可以在 时间内计算出上述和式的值.
数论分块常与 莫比乌斯反演 等技巧结合使用.
思路
首先,本文通过一个简单的例子来说明数论分块的思路.假设要计算如图所示的双曲线下的整点个数:
这相当于在计算和式:
这是前文所示和式在 时的特殊情况.
最简单的做法当然是逐列计算然后求和,但是这样需要计算第 列中每列整点的个数.观察图示可以发现,这些整点列的可以分成 块,每块内点列的高度是一致的,形成一个矩形点阵.所以,只要能够知道这些块的宽度,就能够通过计算这些矩形块的大小快速完成统计.
这就是整除分块的基本思路.
性质
本节讨论关于双曲线 下整点分块的若干结论.具体地,需要将 之间的整数按照 的取值分为若干块.设
这就是 所有可能的取值集合.
首先,这样的不同取值只有 个.所以,数论分块得到的块的数目只有 个.
性质 1
.
证明
分两种情况讨论:
- 当 时, 的取值至多 个,所以 的取值也至多只有 个.
- 当 时, 也至多只有 个取值.
因此,所有可能取值的总数 .
通过更细致的分析,实际上可以精确地描述集合 及其大小.
性质 2
设 .集合 中的元素从小到大依次是
进而,.
证明
首先,对于 ,可以证明 .令 ,需要证明 .写成不等式,这相当于已知 ,需要证明 .已知条件可以写作 ,因此,只需要证明 ,亦即 .这等价于 ,这对于所有 都成立.
这一结果实际上说明,映射 构成集合 和集合 之间的双射.由于 各不相同, 也各不相同.两个集合唯一有可能相同的元素就是 与 .所以,.
要得到 最终的表达式,需要考察何时 成立.
- 当 时,总有 ,亦即 .此时,有 .又因为 一定是奇数,左侧可以等价地松弛到 ,所以该条件就等价于 ,亦即 .
- 当 时,总有 ,亦即 .又因为 ,就有 .这就等价于 .再次利用 是奇数这一点,右侧可以等价地收紧到 ,所以该条件就等价于 ,亦即 .
总结这两种情形,就得到 成立.
然后,单个块的左右端点都很容易确定.
性质 3
对于 ,所有满足 的整数 的取值范围为
证明
因为 相当于不等式
这进一步等价于
利用 这一点,可以对该不等式取整,它就等价于
这一性质还体现了图像的对称性:每个块的右端点(前文图中的绿点)的集合恰为 .这很容易理解,因为整个图像关于直线 是对称的.
除了这些性质外,集合 还具有良好的递归性质:
性质 4
对于 ,有 .
证明
前文已经提到, 既是每块中 的取值集合,也是每块的右端点集合.这意味着,如果要递归地应用数论分块(即函数在 处的值依赖于它在 处的值),那么在整个计算过程中所涉及的取值集合和右端点集合,其实都是 .一个典型的例子是 杜教筛.
过程
利用上一节叙述的结论,就得到数论分块的具体过程.
为计算和式
的取值,可以依据 的取值将标号 分块.因为 取值相同的标号是一段连续整数 ,所以该块中和式的取值为
为了快速计算该和式,通常需要能够快速计算左侧关于 的和式.有些时候,该和式的表达式是已知的,可以在 时间内完成单次计算;有些时候,可以预处理出它的前缀和,仍然可以在 时间内完成单次查询.
在顺次计算每块左右端点时,当前块的左端点 就等于前一块的右端点再加 ,而当前块的右端点就等于 .由此,可以得到如下伪代码:
假设单次计算 的时间复杂度为 ,则整个过程的时间复杂度为 .
扩展
前文讨论的是数论分块的最常见也最基本的形式.本节进一步讨论数论分块的扩展形式.
向上取整的数论分块
数论分块可以用于计算含有向上取整的和式:
因为 ,该和式可以转化为向下取整的情形:
注意到求和的上限发生了变化,以及 时单独的一项.
多维数论分块
数论分块还可以用于处理包含不只有一个取整式的和式:
为了应用数论分块的思想,需要保证每块中所有取整式 的取值都不发生变化,也就是说,多维的块应当是所有一维的块的交集.为此,对于已知的左端点 ,相应的右端点为
也就是说,对于所有一维分块的右端点取最小值,作为多维分块的右端点.可以借助下图理解:
较为常见的是二维形式,此时可将前述伪代码中 替换成
任意指数数论分块
数论分块可以用于含有任意指数的取整式的和式计算:
其中, 为正实数.本文讨论的基本形式中,.
性质
对于正整数 和正实数 ,设
那么,有
.
对于 ,使得 成立的 的取值范围为
证明
对于第一点,分两种情况:
- 当 时,有 ,所以 至多有 种取值.
- 当 时,有 ,进而有 ,所以 也至多只有 种取值.
综合两种情形,就有 .
对于第二点, 就等价于
对该不等式取整,就得到第二个命题.
利用这些性质,就可以在 的时间复杂度下实现对任意指数的数论分块.
例子
例如,对于 时的如下和式
可以通过数论分块在 时间内解决.已知块的左端点为 时,可以计算右端点为 .
例题
组数据,每组一个整数 .对于每组数据,输出 .
解答
根据前文分析,可以对于每一块相同的 一起计算.时间复杂度为 .
实现
#include <iostream> long long H(int n) { long long res = 0; // 储存结果 int l = 1, r; // 块左端点与右端点 while (l <= n) { r = n / (n / l); // 计算当前块的右端点 // 累加这一块的贡献到结果中。乘上 1LL 防止溢出 res += 1LL * (r - l + 1) * (n / l); l = r + 1; // 左端点移到下一块 } return res; } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int t, n; std::cin >> t; while (t--) { std::cin >> n; std::cout << H(n) << '\n'; } return 0; }
有一排 只怪兽,每只怪兽初始血量为 .一次攻击会使一段连续的存活的怪兽血量减 ,血量不大于 视作死亡.对于所有 求出击杀所有怪兽所需攻击次数.其中,.
解答
令 .假设击杀所有前 只怪兽需要 次攻击,第 只怪兽的血量为 .由于击杀第 只怪兽时,需要攻击它 次,这些攻击都可以延伸到第 只怪兽.因此,要击杀第 只怪兽,只需要再攻击 次.由此,总的攻击次数为
由于题目涉及的 都比较大,对每个 分别计算该和式并不可行.可以考虑对每个 ,都维护数列 .初始时,设 .假设数列 已知,考虑如何对它进行修改才能得到数列 .根据前文分析,只需要对数列的第 项增加 即可.利用二维数论分块,可以将这一修改操作拆分成 段区间修改操作,且每段区间上增加的值是固定的.最后得到的数列 就是答案.
由于题目涉及一系列区间加操作,且查询只发生所有修改完成后.所以,可以通过维护差分序列进行区间加修改,最后通过求前缀和得到所求数列.总的时间复杂度为 .本题也存在其他解法.
实现
#include <algorithm> #include <iostream> constexpr int N = 100009; int n, a[N], maxn; long long ans[N]; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cin >> n; for (int i = 1; i <= n; ++i) { std::cin >> a[i]; maxn = std::max(maxn, a[i]); } for (int i = 0; i < n; ++i) for (int l = 1, r;; l = r + 1) { r = std::min(l < a[i] ? (a[i] - 1) / ((a[i] - 1) / l) : N, l < a[i + 1] ? (a[i + 1] - 1) / ((a[i + 1] - 1) / l) : N); // 二维数论分块 if (r == N) break; int x = (a[i + 1] - 1) / l - std::max(a[i] - 1, 0) / l; if (x > 0) ans[l] += x, ans[r + 1] -= x; // 累加贡献 } ++ans[0]; // ⌈a/l⌉=(a-1)/l+1的式子当a=0时不成立,需要修正 for (int i = 1; i <= maxn; ++i) std::cout << (ans[i] += ans[i - 1]) << " \n"[i == maxn]; return 0; }