【数论优化】从 O (n²) 到 O (√n):因数平方和求和问题的三重解法

因数平方和求和优化

在算法竞赛和数论问题中,“求和类” 问题的优化往往能体现对数学规律的理解深度。本文以「因数平方和求和」问题为例,从暴力解法出发,逐步优化到线性时间,最终通过数论分块实现最优化。

一、问题背景

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²=1
  • f(2)=1²+2²=5
  • f(3)=1²+3²=10
  • g(3)=1+5+10=16

二、解法一:暴力枚举(O (n²))

1. 核心思路

最直观的想法:对每个数 i(1≤i≤n),枚举其所有因数 j(1≤j≤i),若 j 是 i 的因数,则将  累加到结果中。

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暴力解法耗时线性解法耗时数论分块耗时
1e31ms0ms0ms
1e48ms0ms0ms
1e578ms1ms0ms
1e67812ms5ms0ms
1e7超时45ms0ms
1e10超时超时1ms

核心对比表

解法时间复杂度空间复杂度适用场景核心思想
暴力枚举O(n²)O(1)n≤1e3,理解问题本质逐个枚举每个数的因数
线性优化O(n)O(1)n≤1e6,简单高效转换视角,计算每个 k 的贡献次数
数论分块O(√n)O(1)n≤1e12,极致优化批量处理 n//k 相同的区间

六、总结

从暴力到线性,再到数论分块,优化的核心是 发现数学规律,减少重复计算

  1. 暴力解法是基础,但效率极低,仅用于理解问题本质;
  2. 线性解法通过 “转换求和视角”,将重复的因数枚举转化为单次计算,效率提升一个量级;
  3. 数论分块则进一步挖掘 “整除值的区间性”,将线性遍历压缩为根号级,是处理大数求和的经典技巧。

掌握数论分块不仅能解决本题,还能推广到所有形如 ∑(k=1到n) f(k) × (n//k) 的求和问题,是算法竞赛中必备的数论技巧。所有代码均可直接编译运行,建议结合不同规模的 n 测试,直观感受三种解法的效率差异。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值