2 条题解
-
2
题解:线性筛法求1~n的约数个数之和
一、欧拉筛的核心原理
欧拉筛(线性筛) 是一种高效的质数筛选算法,核心思想是 每个合数仅被其最小质因子筛除一次。
- 为什么用欧拉筛:
- 时间复杂度最优:传统埃氏筛的时间复杂度为 ,而欧拉筛为 ,适合处理大规模数据(如 )。
- 记录关键信息:在筛质数的同时,可以记录每个数的最小质因子及其幂次,为后续计算约数个数提供数据支持。
二、为什么用欧拉筛求约数个数
约数个数公式:
其中 是质因数分解中各质数的指数。
欧拉筛的优势:- 动态维护最小质因子:通过记录每个数的最小质因子幂次
num[i],可以在筛的过程中直接更新约数个数d[i]。 - 避免重复计算:每个合数仅被处理一次,保证时间复杂度为 。
三、代码详细解释
#include<bits/stdc++.h> using namespace std; int n; vector<bool> isPrime(1000100, true); // 质数标记数组 vector<int> d(1000100, 1); // 约数个数数组 vector<int> num(1000100, 0); // 最小质因子幂次数组 int countDivisors() { isPrime[0] = isPrime[1] = false; // 0和1不是质数 d[1] = 1; // 1的约数个数为1 vector<int> primes; // 存储质数列表 for (int i = 2; i <= n; i++) { // 从2开始遍历每个数 if (isPrime[i]) { // 如果i是质数 primes.push_back(i); // 加入质数列表 d[i] = 2; // 质数的约数个数为2(1和自身) num[i] = 1; // 质数的最小质因子幂次为1 } // 用当前质数p筛去i*p for (int j = 0; j < primes.size() && i * primes[j] <= n; j++) { isPrime[i * primes[j]] = false; // 标记合数 if (i % primes[j] == 0) { // primes[j]是i的最小质因子 num[i * primes[j]] = num[i] + 1; // 幂次+1 // 动态调整约数个数:d[i*p] = d[i]/(e+1) * (e+2) d[i * primes[j]] = d[i] / (num[i] + 1) * (num[i * primes[j]] + 1); break; // 关键:确保每个合数只被筛一次 } else { // primes[j]是i*p的最小质因子 num[i * primes[j]] = 1; // 新质因子,幂次为1 d[i * primes[j]] = d[i] * d[primes[j]]; // 约数个数相乘 } } } // 累加所有数的约数个数 int ans = 0; for (int i = 1; i <= n; i++) ans += d[i]; return ans; } int main() { cin >> n; cout << countDivisors(); return 0; }
四、关键步骤解析
-
初始化:
isPrime标记所有数为质数,后续逐步筛除合数。d[1] = 1:显式设置1的约数个数。
-
质数处理:
- 当
i是质数时,加入质数列表primes,并初始化d[i] = 2(质数的约数个数为2)。
- 当
-
合数筛除与约数计算:
- 内层循环:遍历质数表,筛去
i * primes[j]。 - 整除判断:
- 若
i % primes[j] == 0,说明primes[j]是i的最小质因子。- 更新
num[i*p]为num[i]+1(幂次增加)。 - 动态调整
d[i*p]
- 更新
- 否则,
primes[j]是i*p的最小质因子,直接相乘约数个数。
- 若
- 内层循环:遍历质数表,筛去
-
结果累加:
- 遍历
d[1..n],累加所有数的约数个数。
- 遍历
- 为什么用欧拉筛:
-
1
数学原理详解
本题要求计算和式 , 其中 表示正整数 的约数个数。直接对每个 计算约数个数再求和的方法时间复杂度为 ,当 较大时(如 )效率较低。因此,我们需要一种更高效的数学方法。
关键变换:交换求和顺序
考虑所有从 到 的整数,每个数 的约数个数 可以理解为所有能整除 的约数 的个数。
因此,原和式可以重写为:
=
就是k 整除 i
这里内层求和表示对每个 ,计算其约数的个数。现在,我们交换求和顺序:改为先固定约数 ,然后计算 作为约数出现在多少个 中(其中 )。由于 是 的约数当且仅当 是 的倍数,因此对于每个 ,满足 且 的 的个数就是 的倍数中不超过 的个数,即 。
换句话说:
- 正是 在范围 到 中作为约数出现的次数(即“有效的个数”)。例如,如果 和,那么 的倍数有,所以出现了 2 次,而。
因此,求和 实际上是在对所有可能的约数 计算其出现的频次,从而等价于原问题。这种变换是数论中常见的技巧,称为“除数求和”或“交换求和顺序”。
因此,原和式等价于:
=
这个变换将问题转化为计算所有 从 到 的 之和。
效率分析
直接计算新和式的时间复杂度为 ,对于 是可行的。但还可以进一步优化:由于 的值对于连续的 可能相同,我们可以找到使值相同的区间,批量计算。具体来说,对于每个 ,存在最大的 使得
=,则 。
这样,求和的时间复杂度可降为 。但对于本题范围,直接循环 从 到 也已足够。
样例验证
取输入样例 :
- 直接计算:
- ,,,
- 和为 。
- 用新方法:
- ,,,
- 和为 。结果一致。
- 1
信息
- ID
- 5610
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 3
- 已通过
- 2
- 上传者