数论分块
数论分块
数论分块可以快速计算一些含有除法向下取整的和式 (即形如 \(\sum_{i=1}^{n} f(i) g\left(\left\lfloor\dfrac{n}{i}\right\rfloor\right)\) 的和式)。当可以在 \(O(1)\) 内计算 \(f(r)-f(l)\) 或已经预处理出 \(f\) 的前缀和时,数论分块就可以在 \(O(\sqrt{n})\) 的时间内计算上述和式的值。
它主要利用了富比尼定理 (\(Fubini's theorem\)),将 \(\big\lfloor\dfrac{n}{i}\big\rfloor\) 相同的数打包同时计算。
引入
题目转换一下就是求 \(f(n)=\sum_{i=1}^{n}\big\lfloor\dfrac{n}{i}\big\rfloor\)
朴素做法,遍历 \(1\sim n\) 求和,时间复杂度 \(O(n)\)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int ans = 0;
for (int i = 1; i <= n; ++i) {
ans += n / i;
}
System.out.println(ans);
}
}
不能解决 \(10^9\) 以上的数据范围,考虑优化
当 \(n=21\) 时,\(\big\lfloor\dfrac{n}{i}\big\rfloor\) 的值如下:
\(i\) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
\(\big\lfloor\dfrac{n}{i}\big\rfloor\) | 21 | 10 | 7 | 5 | 4 | 3 | 3 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
观察发现,\(\big\lfloor\dfrac{n}{i}\big\rfloor\) 的取值在连续的一段区间内是相同的,是”一块一块“的
如果我们知道了每一块的值和长度(左右边界),也就可以使用乘法运算来代替加法运算了
求某一值所在块的右端点
假设求 \(i\) 所在块的右端点
\(i\) 所在块的值为 \(k=\big\lfloor\dfrac{n}{i}\big\rfloor\),则 \(k\leqslant\dfrac{n}{i}\),所以 \(\big\lfloor\dfrac{n}{k}\big\rfloor\geqslant\left\lfloor\dfrac{n}{\frac{n}{i}}\right\rfloor=\big\lfloor i\big\rfloor=i\)
因此,\(i_{\max }=\left\lfloor\dfrac{n}{k}\right\rfloor=\left\lfloor\dfrac{n}{\left\lfloor\dfrac{n}{i}\right\rfloor}\right\rfloor\),即右端点为 \(\left\lfloor\dfrac{n}{\left\lfloor\dfrac{n}{i}\right\rfloor}\right\rfloor\)
实现
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextLong();
long ans = 0;
for (long l = 1, r; l <= n; l = r + 1) {
//取min防止越界
r = Math.min(n / (n / l),n);
ans += n / l * (r - l + 1);
}
System.out.println(ans);
}
}
时间复杂度分析
\(\text{ 分块的块数 }\leq 2\left\lfloor\sqrt{n}\right\rfloor\)
证明:
当 \(i \leq\lfloor\sqrt{n}\rfloor\) 时, \(\left\lfloor\dfrac{n}{i}\right\rfloor\) 有 \(\lfloor\sqrt{n}\rfloor\) 种取值。
当 \(i>\lfloor\sqrt{n}\rfloor\) 时, \(\left\lfloor\dfrac{n}{i}\right\rfloor \leq\lfloor\sqrt{n}\rfloor,\left\lfloor\dfrac{n}{i}\right\rfloor\) 至多有 \(\lfloor\sqrt{n}\rfloor\) 种取值。综上,分块的块数\(\leq 2\left\lfloor\sqrt{n}\right\rfloor\)
因此,时间复杂度为 \(O(2\sqrt n)=O(\sqrt n)\)
例题
约数和
题意概述
原题链接:P2424 约数和 - 洛谷
给定正整数 \(x\),\(y\),\(x<y\),求 \(\sum_{i=x}^{y}\sum_{d|i}d\)
解题思路
求 \([x,y]\) 的函数值可以先求得 \([1,x-1]\) 与 \([1,y]\) 再相减,即\(\sum_{i=x}^{y}\sum_{d|i}d=\sum_{i=1}^{y}\sum_{d|i}d-\sum_{i=1}^{x-1}\sum_{d|i}d\)
因此,该问题变成了求出 \(\sum_{i=1}^{n}\sum_{d|i}d\)
与洛谷 P1403 约数研究类似,求每个数的约数和,可以转换成求 \(1\sim n\) 中有哪些数含 \(i\),其中 \(i\in[1,n]\),换言之,转化为枚举因子 \(d\)
\(1\sim n\) 的约数中有 \(\lfloor\dfrac{n}{d}\rfloor\) 个 \(d\) ,则 \(d\) 对答案的贡献为 \(\lfloor\dfrac{n}{d}\rfloor\times d\)
因此,式子可以转化为 \(\sum_{d=1}^{n}\lfloor\dfrac{n}{d}\rfloor\times d\)
因为乘法满足分配律,对于 \(\lfloor\dfrac{n}{d}\rfloor\) 值相等的一块区间,满足 \(\sum_{d=l}^r\lfloor\dfrac{n}{d}\rfloor\times d=\lfloor\dfrac{n}{d}\rfloor\times\sum_{d=l}^rd=\lfloor\dfrac{n}{d}\rfloor\times\dfrac{(l+r)\times(r-l+1)}{2}\)
Code
import java.util.Scanner;
public class Main {
//计算 1~n 所有约数的和
static long calculate(long n) {
long ans = 0;
for (long l = 1, r; l <= n; l = r + 1) {
r = Math.min(n / (n / l), n);
ans += (l + r) * (r - l + 1) / 2 * (n / l);
}
return ans;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int x = sc.nextInt(), y = sc.nextInt();
System.out.println(calculate(y) - calculate(x - 1));
}
}
余数求和
题意概述
原题链接:P2261 余数求和 - 洛谷
给定正整数 \(n\),\(k\),求 \(f(n,k)=\sum_{i=1}^{n}k\ mod\ i\)
解题思路
\[\begin{equation} \begin{aligned} f(n,k)&=\sum_{i=1}^{n}k\ mod\ i \\ &=\sum_{i=1}^{n}(k-\lfloor\dfrac{k}{i}\rfloor\times i)\\ &=\sum_{i=1}^{n}k-\sum_{i=1}^{n}\lfloor\dfrac{k}{i}\rfloor\times i\\ &=n\times k-\sum_{i=1}^{n}\lfloor\dfrac{k}{i}\rfloor\times i \end{aligned} \end{equation}\]
和式部分与上一题仅为分子变了
Code
注意点:
- 数据范围,两数相乘可能超出 \(int\) 范围
- 当 \(k<l\) 时,不能再求右端点,会除零错误
import java.util.Scanner;
public class Main {
//计算 1~n 所有约数的和
static long calculate(long n, long k) {
long ans = 0;
//当左端点小于等于k时,才有右端点
for (long l = 1, r; l <= n && l <= k; l = r + 1) {
r = Math.min(k / (k / l), n);
ans += (l + r) * (r - l + 1) / 2 * (k / l);
}
return ans;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), k = sc.nextInt();
//两数相乘可能超出int
System.out.println((long) n * k - calculate(n, k));
}
}