在算法竞赛和数论问题中,“求和类” 问题的优化往往能体现对数学规律的理解深度。本文以「因数平方和求和」问题为例,从暴力解法出发,逐步优化到线性时间,最终通过数论分块实现最优化。
一、问题背景
1. 问题定义
记 f(x) 为 x 的所有因数的平方和(例如 f(12) = 1² + 2² + 3² + 4² + 6² + 12² = 210),定义 g(n) = ∑(i=1到n) f(i)。给定正整数 n,求 g(n) mod 10⁹+7。
2. 问题分析
直接理解:g(n) 是 1 到 n 每个数的因数平方和的总和。比如 n=3 时:
f(1)=1²=1f(2)=1²+2²=5f(3)=1²+3²=10g(3)=1+5+10=16
二、解法一:暴力枚举(O (n²))
1. 核心思路
最直观的想法:对每个数 i(1≤i≤n),枚举其所有因数 j(1≤j≤i),若 j 是 i 的因数,则将 j² 累加到结果中。
2. 完整代码
import java.util.Scanner;
/**
* 解法一:暴力枚举(O(n²))
* 适合n ≤ 1e3的小数据场景
*/
public class FactorSquareSum_BruteForce {
// 题目要求的模数
static final int MOD = 1000000007;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入正整数n:");
int n = sc.nextInt();
long startTime = System.currentTimeMillis(); // 计时开始
long ans = 0;
// 遍历每个数i,计算f(i)并累加
for (int i = 1; i <= n; i++) {
// 枚举i的所有因数j
for (int j = 1; j <= i; j++) {
if (i % j == 0) {
ans = (ans + (long) j * j) % MOD; // 累加j²并取模,避免溢出
}
}
}
long endTime = System.currentTimeMillis(); // 计时结束
System.out.println("结果:" + ans);
System.out.println("耗时:" + (endTime - startTime) + "ms");
sc.close();
}
}
3. 优缺点分析
- 优点:逻辑简单,完全贴合问题字面意思,适合理解问题本质。
- 缺点:时间复杂度 O (n²),当 n≥1e4 时就会明显卡顿,n=1e5 时基本超时,无法处理大数。
三、解法二:线性优化(O (n))
1. 核心思路
问题转换:暴力解法的瓶颈是 “重复枚举因数”,我们换个角度思考:g(n) 本质是 “1 到 n 中每个数 k 作为因数,在多少个数中出现,再乘以 k² 的总和”。
举个例子:k=2 在 1~6 中作为因数出现在 2、4、6 中,共 3 次(即 6//2=3),因此贡献为 2² × 3 = 12。
通用规律:对于任意 k∈[1,n],k 作为因数出现的次数为 n//k(k 的倍数有 k,2k,...,mk≤n,共 m=n//k 个)。因此:g(n) = ∑(k=1到n) (k² × (n//k))
2. 完整代码
import java.util.Scanner;
/**
* 解法二:线性优化(O(n))
* 适合n ≤ 1e6的中等数据场景
*/
public class FactorSquareSum_Linear {
static final long MOD = 1000000007L;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入正整数n:");
long n = sc.nextLong(); // 用long适配更大的n
long startTime = System.currentTimeMillis();
long ans = 0;
// 遍历每个k,计算k的贡献:k² × (n//k)
for (long k = 1; k <= n; k++) {
long k2 = (k * k) % MOD; // 计算k²并取模,避免溢出
long cnt = n / k; // k作为因数出现的次数
ans = (ans + k2 * cnt) % MOD; // 累加贡献并取模
}
long endTime = System.currentTimeMillis();
System.out.println("结果:" + ans);
System.out.println("耗时:" + (endTime - startTime) + "ms");
sc.close();
}
}
3. 优缺点分析
- 优点:时间复杂度降至 O (n),n=1e6 时可轻松通过,空间复杂度 O (1),实现简单。
- 缺点:当 n≥1e8 时,O (n) 的时间复杂度仍会超时(遍历 1e8 次需要数秒),需要进一步优化。
四、解法三:数论分块(O (√n))
1. 核心思路
线性解法的瓶颈是 “逐个遍历 k”,而数论分块(整除分块)的核心是:n//k 的值在连续区间内是相同的,我们可以对这些区间批量计算,无需逐个遍历。
关键规律
对于当前左端点 l,存在最大的右端点 r,使得 n//l = n//r,且 r = n/(n//l)。例如 n=12 时:
- l=1,n//l=12 → r=12/12=1(区间 [1,1],n//k=12)
- l=2,n//l=6 → r=12/6=2(区间 [2,2],n//k=6)
- l=5,n//l=2 → r=12/2=6(区间 [5,6],n//k=2)
区间平方和公式
要批量计算区间 [l,r] 的 k² 和,需用到平方和公式:1²+2²+...+x² = x(x+1)(2x+1)/6因此区间 [l,r] 的平方和为:sum = [r(r+1)(2r+1) - (l-1)l(2l-1)] / 6
由于涉及模运算,除法需用模逆元处理(费马小定理:若 MOD 是质数,则 1/a mod MOD = a^(MOD-2) mod MOD)。本题中 MOD=1e9+7 是质数,因此 6 的逆元为 pow(6, MOD-2, MOD)。
2. 完整代码
import java.util.Scanner;
/**
* 解法三:数论分块(O(√n))
* 适合n ≤ 1e12的超大数场景
*/
public class FactorSquareSum_Block {
static final long MOD = 1000000007L;
// 预处理6的模逆元:6^(MOD-2) mod MOD(费马小定理)
static final long INV6 = pow(6, MOD - 2);
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入正整数n:");
long n = sc.nextLong();
long startTime = System.currentTimeMillis();
long ans = 0;
// 数论分块核心循环:遍历每个区间[l, r]
for (long l = 1, r; l <= n; l = r + 1) {
long q = n / l; // 当前区间的n//k值(所有k∈[l,r]的n//k都等于q)
r = n / q; // 找到该值的最大右端点
// 计算区间[l, r]的平方和:squareSum(r) - squareSum(l-1)
long sumSquare = (squareSum(r) - squareSum(l - 1)) % MOD;
// 累加贡献:区间平方和 × q,处理负数(避免模后为负)
ans = (ans + sumSquare * q % MOD) % MOD;
}
// 确保结果非负(减法可能导致负数)
ans = (ans + MOD) % MOD;
long endTime = System.currentTimeMillis();
System.out.println("结果:" + ans);
System.out.println("耗时:" + (endTime - startTime) + "ms");
sc.close();
}
/**
* 计算1~x的平方和 mod MOD
* 公式:x*(x+1)*(2x+1)/6
*/
private static long squareSum(long x) {
x %= MOD; // 先取模避免溢出(x可能很大)
long term1 = x * (x + 1) % MOD;
long term2 = (2 * x + 1) % MOD;
long sum = term1 * term2 % MOD;
sum = sum * INV6 % MOD; // 乘以6的逆元,等价于除以6
return sum;
}
/**
* 快速幂:计算a^b mod MOD
* 用于求模逆元
*/
private static long pow(long a, long b) {
long res = 1;
a %= MOD; // 底数先取模
while (b > 0) {
if ((b & 1) == 1) { // 二进制位为1时,乘上当前底数
res = res * a % MOD;
}
a = a * a % MOD; // 底数平方
b >>= 1; // 指数右移一位(除以2)
}
return res;
}
}
3. 优缺点分析
- 优点:时间复杂度降至 O (√n),n=1e12 时仅需遍历约 1e6 次,是处理大数的最优解法。
- 缺点:需要理解数论分块和模逆元的概念,实现稍复杂,需注意溢出和负数处理。
五、三种解法对比测试
模拟时间表
| 输入 n | 暴力解法耗时 | 线性解法耗时 | 数论分块耗时 |
|---|---|---|---|
| 1e3 | 1ms | 0ms | 0ms |
| 1e4 | 8ms | 0ms | 0ms |
| 1e5 | 78ms | 1ms | 0ms |
| 1e6 | 7812ms | 5ms | 0ms |
| 1e7 | 超时 | 45ms | 0ms |
| 1e10 | 超时 | 超时 | 1ms |
核心对比表
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 | 核心思想 |
|---|---|---|---|---|
| 暴力枚举 | O(n²) | O(1) | n≤1e3,理解问题本质 | 逐个枚举每个数的因数 |
| 线性优化 | O(n) | O(1) | n≤1e6,简单高效 | 转换视角,计算每个 k 的贡献次数 |
| 数论分块 | O(√n) | O(1) | n≤1e12,极致优化 | 批量处理 n//k 相同的区间 |
六、总结
从暴力到线性,再到数论分块,优化的核心是 发现数学规律,减少重复计算:
- 暴力解法是基础,但效率极低,仅用于理解问题本质;
- 线性解法通过 “转换求和视角”,将重复的因数枚举转化为单次计算,效率提升一个量级;
- 数论分块则进一步挖掘 “整除值的区间性”,将线性遍历压缩为根号级,是处理大数求和的经典技巧。
掌握数论分块不仅能解决本题,还能推广到所有形如 ∑(k=1到n) f(k) × (n//k) 的求和问题,是算法竞赛中必备的数论技巧。所有代码均可直接编译运行,建议结合不同规模的 n 测试,直观感受三种解法的效率差异。
因数平方和求和优化
1580

被折叠的 条评论
为什么被折叠?



