1. 入门题不入门:求质数因数背后的真实考点
"十五.求质数因数"这个标题,放在算法题库里显得平平无奇,但真正动手用C++实现过的人都知道,这道题是从"会写循环"到"懂算法"的一道分水岭。尤其是配合"c++质数口袋"这类热词来讨论,说明很多人都卡在了同一个点上:怎么在有限的运行时间内,把一个大整数的所有质因数找出来。
先明确一下需求。质因数分解,就是给定一个正整数n,输出它的所有质因子。比如n=84,结果是2、2、3、7,如果按去重算就是2、3、7。这里有个细节很多人一开始没想清楚:到底要不要输出重复的质因子?这个会在下文专门说。核心问题是,n的范围如果很小,比如100以内,套两层循环暴力试除就完事了;但如果n是10^12级别,甚至10^18级别,暴力法就直接超时。考的不是你会不会写循环,而是你会不会把循环的次数降下来。
我最初带新人的时候,发现大家普遍有个误区:觉得"求质数因数"是一道基础题,随便写写就行。实际上,它同时考察了质数判定、整除性质、边界处理、代码常数优化,以及在不同数据范围下选择不同算法的工程判断力。一个合格的实现,要能让n从2到10^12都稳稳跑完,还要考虑n本身就是质数、n是1、n是奇数平方数等边界情况。
这篇文章就从最简单的暴力实现开始,一步步推导出优化的思路,最后给出一套可以直接用在竞赛和大数场景里的C++模板。每段代码我都会标注复杂度,并解释为什么这样写,而不是那样写。有基础的话,可以直接跳到第3节看实现细节;如果是初学者,建议从第2节把数学原理吃透,这部分搞明白了,后面的代码就完全是水到渠成的事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力试除的边界在哪里:从数学原理推导出sqrt(n)上限
2.1 最朴素的想法是怎么来的
第一次碰这道题的人,思路基本都是这样的:从2开始,逐一判断i能不能整除n,如果能整除,就说明i是一个因数,然后判断i本身是不是质数。代码写出来,大概长这样:
cpp复制#include <bits/stdc++.h>
using namespace std;
bool isPrime(long long x) {
if (x < 2) return false;
for (long long i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
void bruteForce(long long n) {
for (long long i = 2; i <= n; i++) {
if (n % i == 0 && isPrime(i)) {
cout << i << " ";
}
}
cout << endl;
}
这段代码在n是10^6以下时还能勉强跑,但到了10^7以上就开始卡顿,到10^12就完全没法用了。原因是它的时间复杂度是O(n·√n):外层循环要跑n次,每次还要用O(√n)去判断质数,整体是O(n√n),这个增长曲线非常陡峭。很多初学者在这里栽了跟头,不是逻辑错了,而是没算清楚复杂度。
那么这个暴力思路有没有价值呢?有。它给了我们一个正确的出发点——枚举因子。
2.2 为什么只需要试除到√n
优化的第一个突破口,来自一个非常基础的数学事实:如果n有一个大于√n的因子d,那么n/d一定小于√n。比如n=42,√n≈6.48,它的因子7大于√n,但配对因子42/7=6就小于√n。所以,只要在2到√n之间找不到任何因子,那么n在√n到n之间也必然不存在因子,n就是一个质数。
这是我们做质因数分解的最核心依据。明白这一点之后,试除循环的终止条件就可以从n变成√n。仅这一个改动,时间复杂度就从O(n√n)降到O(√n·something),对于10^12的n来说,循环次数从1万亿级别直接降到100万级别。
这里我建议一个优化写法:用除法防止乘法溢出。
cpp复制// 不推荐
for (long long i = 2; i * i <= n; i++)
// 推荐
for (long long i = 2; i <= n / i; i++)
如果你在刷题平台提交过代码,大概率见过ii导致的int溢出问题。n本身是int时,ii会先按int计算再比较,一旦ii超过2^31-1,结果可能变成负数,循环条件就废了。用n/i代替ii,就完全规避了这个坑,还顺便省了一次乘法运算,一举两得。
2.3 每找到一个因子,n就可以缩小
还有一个经常被忽略的优化:我们不用每次都拿原始n去试除。当找到因子i时,让n不断除以i,直到不能整除为止。这样做有三个好处:
- 已经分解出来的因子不会在后面的循环里再被重复考虑,因为n已经变小了;
- 后续判断的n本身变成了剩余部分,试除范围也随之缩小;
- 如果某个因子出现多次(比如8=2×2×2),用一个while循环就能统计出次数。
这样改了之后,我们连"判断i是否为质数"的步骤都可以省掉。原因是,如果一个合数能整除n,它的质因子一定更小,而更小的质因子在之前已经被除干净了,所以这个合数根本不可能再整除当前的n。这理解起来有点像"先解决小问题,大问题自然不存在",是质因数分解优化中最巧妙的一环。
cpp复制void trialDivisi
