一、题目理解
核心要求 :
给定正整数 N(1 \< N \< 10\^9),计算其质因数的总个数(含重复计数) 。
例如:120 = 2 \\times 2 \\times 2 \\times 3 \\times 5 → 输出 5。
输入输出:
- 输入:多组测试数据,每行一个正整数 N
- 输出:每组数据输出对应的质因数个数
关键约束:
- N 的范围:1 \< N \< 10\^9
- 可能有多组测试数据
- 需要高效处理大数情况
二、解题思路演进
方法一:暴力枚举(不可行)
思路:从 2 到 N-1 逐个检查是否为因数,如果是则继续分解。
问题:
- 时间复杂度:O(N)
- 当 N = 10\^9 时,需要循环近 10\^9 次,超时
- 无法在 1 秒内完成
方法二:优化试除法(当前代码思路)
核心思想:利用数学性质优化
关键观察:
- 质因数范围限制:任何合数 N 的最小质因数必然 ≤ \\sqrt{N}
- 大质因数唯一性:如果 N 有大于 \\sqrt{N} 的质因数,则最多只有一个
算法步骤:
- 从 2 开始试除,直到 \\sqrt{N}
- 每找到一个因数 i,循环除尽所有 i 因子(保证重复计数)
- 循环结束后,如果 N \> 1,说明剩余部分是大质因数
正确性证明:
- 假设 N = p_1 \\times p_2 \\times ... \\times p_k(p_i 为质数)
- 如果所有 p_i \\leq \\sqrt{N},则循环会找到所有因子
- 如果存在 p_i \> \\sqrt{N},则这样的 p_i 最多一个(否则乘积 > N)
- 循环结束后剩余的 N 即为这个大质因数
三、算法设计
使用的算法
- 试除法(Trial Division)
- 贪心策略:从小到大找最小质因数
关键步骤拆解
cpp
1. 初始化计数器 count = 0
2. 从 i = 2 到 √N 循环:
a. 如果 i 能整除 N:
- 循环除以 i,每次 count++
- 直到 N 不能被 i 整除
3. 如果循环结束后 N > 1:
- 说明剩余的是大质因数,count++
4. 返回 count
四、代码解析
提供的代码分析
cpp
#include<iostream>
#include<cmath>
using namespace std;
int ans(int n){
int count=0;
for(int i=2; i<sqrt(n); i++){ // ⚠️ 这里有bug!
while(n%i==0){
count++;
n/=i;
}
}
if(n>1)
count++;
return count;
}
代码Bug分析
问题 :i < sqrt(n) 应该改为 i * i <= n 或 i <= sqrt(n)
原因:
- 边界问题 :当 \\sqrt{N} 是整数时,
i < sqrt(n)会漏掉 i = \\sqrt{N} 的情况 - 动态变化 :
n在循环中不断变小,每次调用sqrt(n)效率低 - 浮点精度 :
sqrt()返回浮点数,可能存在精度问题
举例说明:
- N = 25,\\sqrt{25} = 5
i < sqrt(25)即i < 5,循环只到 i=4- 会漏掉因子 5,导致错误结果
修正后的代码
cpp
#include<iostream>
using namespace std;
int ans(int n){
int count = 0;
// 优化1:使用 i * i <= n 避免浮点运算
for(int i = 2; i * i <= n; i++){
while(n % i == 0){
count++;
n /= i;
}
}
// 处理剩余的大质因数
if(n > 1)
count++;
return count;
}
int main(){
int n;
while (cin >> n) {
cout << ans(n) << endl;
}
return 0;
}
代码逐行解释
| 行号 | 代码 | 说明 |
|---|---|---|
| 1-2 | #include... |
引入必要头文件 |
| 4 | int ans(int n) |
定义分解函数 |
| 5 | int count = 0; |
初始化计数器 |
| 8 | for(int i = 2; i * i <= n; i++) |
关键:只试除到 √n |
| 9-12 | while(n % i == 0) |
关键:除尽所有 i 因子 |
| 15-16 | if(n > 1) count++; |
关键:处理剩余大质因数 |
| 19-24 | main() |
处理多组输入 |
难点与易错点
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 循环条件 | i < sqrt(n) 会漏边界 |
改为 i * i <= n |
| 重复计数 | 需要 while 循环除尽 | 不能用 if 只除一次 |
| 大质因数 | 循环后剩余部分需处理 | if(n > 1) count++ |
| 效率问题 | 频繁调用 sqrt() | 用 i * i <= n 替代 |
| 整数溢出 | i * i 可能超范围 |
使用 long long 转换 |
五、复杂度分析
时间复杂度
单次查询:O(\\sqrt{N})
详细分析:
- 最坏情况:N 是质数,需要试除到 \\sqrt{N}
- 当 N = 10\^9 时,\\sqrt{N} \\approx 31623
- 实际运行次数远小于理论值(找到因子后 n 会变小)
多组数据:O(T \\times \\sqrt{N}),其中 T 为测试数据组数
空间复杂度
O(1) - 仅使用常数级额外空间
六、优化方案对比
方案一:基础优化(当前方案)
cpp
int ans(int n){
int count = 0;
for(int i = 2; i * i <= n; i++){
while(n % i == 0){
count++;
n /= i;
}
}
if(n > 1) count++;
return count;
}
优点 :代码简洁,易于理解
缺点:会检查一些合数(如 4, 6, 8, 9...)
方案二:跳过偶数优化
cpp
int ans(int n){
int count = 0;
// 先处理因子2
while(n % 2 == 0){
count++;
n /= 2;
}
// 只检查奇数
for(int i = 3; i * i <= n; i += 2){
while(n % i == 0){
count++;
n /= i;
}
}
if(n > 1) count++;
return count;
}
优化效果:
- 减少约 50% 的循环次数
- 避免检查所有偶数(除了 2 以外都不是质数)
方案三:质数表预处理(最优)
cpp
#include<iostream>
#include<vector>
using namespace std;
const int LIMIT = 32000; // sqrt(10^9) ≈ 31623
vector<int> primes;
// 埃拉托斯特尼筛法生成质数表
void generatePrimes(){
vector<bool> isPrime(LIMIT + 1, true);
isPrime[0] = isPrime[1] = false;
for(int i = 2; i <= LIMIT; i++){
if(isPrime[i]){
primes.push_back(i);
for(int j = i * i; j <= LIMIT; j += i)
isPrime[j] = false;
}
}
}
int countPrimeFactors(int n){
int count = 0;
for(int p : primes){
if((long long)p * p > n) break;
while(n % p == 0){
count++;
n /= p;
}
}
if(n > 1) count++;
return count;
}
int main(){
generatePrimes(); // 预处理(只执行一次)
int n;
while(cin >> n){
cout << countPrimeFactors(n) << endl;
}
return 0;
}
优化效果:
- 只用质数试除,避免检查合数
- 单次查询仅需约 3400 次操作(\\pi(31623) \\approx 3400)
- 多组数据时优势明显
七、推广应用
1. 完整质因数分解
cpp
vector<pair<int, int>> factorize(int n){
vector<pair<int, int>> result;
for(int i = 2; i * i <= n; i++){
if(n % i == 0){
int count = 0;
while(n % i == 0){
count++;
n /= i;
}
result.push_back({i, count});
}
}
if(n > 1) result.push_back({n, 1});
return result;
}
// 使用示例:factorize(120) →
应用:计算约数个数、最大公约数、最小公倍数等
2. 判断质数
cpp
bool isPrime(int n){
if(n <= 1) return false;
if(n == 2) return true;
if(n % 2 == 0) return false;
for(int i = 3; i * i <= n; i += 2){
if(n % i == 0) return false;
}
return true;
}
3. 计算约数个数
cpp
int countDivisors(int n){
int result = 1;
for(int i = 2; i * i <= n; i++){
if(n % i == 0){
int count = 0;
while(n % i == 0){
count++;
n /= i;
}
result *= (count + 1); // 约数个数公式
}
}
if(n > 1) result *= 2;
return result;
}
// 示例:countDivisors(12) → 6 (1,2,3,4,6,12)
八、总结
核心要点
-
数学性质是关键:
- 质因数 ≤ √N
- 大于 √N 的质因数最多一个
-
算法优化路径:
- 暴力枚举 → 试除到 √N → 跳过偶数 → 质数表预处理
-
代码实现要点:
- 使用
i * i <= n避免浮点运算 - while 循环除尽所有相同因子
- 循环后处理剩余大质因数
- 使用
实际性能对比
| 方案 | N=10\^9 循环次数 | 适用场景 |
|---|---|---|
| 暴力枚举 | ~10\^9 | ❌ 不可行 |
| 基础试除 | ~31623 | ✅ 单次查询 |
| 跳过偶数 | ~15811 | ✅ 推荐 |
| 质数表 | ~3400 | ✅✅ 多组数据 |
最佳实践建议
- 单次查询:使用跳过偶数的方案
- 多组数据:使用质数表预处理方案
- 代码简洁性:基础试除方案已足够
- 竞赛场景:质数表方案最稳定
这道题完美展示了数学洞察力如何指导算法优化,是理解数论算法的经典入门题目!