筛法预处理与快速质因数分解
极难2先造工具再干活:用筛法预处理实现超快质因数分解
质因数分解,就是把一个合数写成几个质数相乘的形式。比如 12 = 2 × 2 × 3。如果只分解一两个数,你可以用“试除法”——从小到大试除质数,但当你需要分解成百上千个数时,每次从头试除就会非常慢。有没有什么好办法,能一口气把一大批数的质因数都快速分解出来呢?
有!筛法预处理就是答案。它就像先建一个“质数工具箱”,把所有能用的小质数提前找出来、排好队,然后分解任何一个数时,直接从工具箱里拿工具,又快又省力。更厉害的是,我们还可以给每个合数记下它的“最小质因子”(Smallest Prime Factor,简称SPF),这样分解时连试除都不需要,直接拿着 SPF 一路除下去就行。
下面我们就来学习这个强大的方法,看完之后你也能写出比试除法快几十倍的质因数分解代码。
一、生活中的类比:先修好“万能钥匙箱”
想象你是一个锁匠,接了一个大活儿:要修理学校里 1000 把锁,每把锁的锁芯结构都不同。如果你每修一把锁就现场车一把钥匙,那得累死。聪明的做法是:先花点时间,把所有可能用到的标准钥匙(即所有小质数)都打造好,放进一个工具箱。然后修锁时,只需从工具箱里拿出合适的钥匙试一下,不行再换下一把,效率就高多了。
筛法预处理就是这个道理。我们先一次性“筛”出某个范围内(比如 1 到 1 000 000)的所有质数,然后把这些质数存起来。以后不管分解哪个数,都只用这些预存的质数去试除,而不用从 2 开始重新判断是不是质数。更高级的做法是:在筛的过程中,顺便记录每个数的最小质因子(SPF),这样分解时连试除都省了,直接“跳”着除。
二、数学原理:从“埃氏筛”到“最小质因子数组”
1. 埃拉托斯特尼筛法(埃氏筛)——古人发明的“找质数神器”
要找出所有小于等于 N 的质数,古希腊数学家埃拉托斯特尼想了个绝妙的主意:
- 先把 2 到 N 的所有数写下来,假设它们都是质数。
- 从第一个质数 2 开始,把 2 的倍数(除了 2 本身)全部划掉(标记为合数)。
- 然后看下一个没被划掉的数 3,它是质数,接着把 3 的倍数(除了 3)全部划掉。
- 继续这样下去,每次找下一个没被划掉的数,它就是质数,再划掉它的倍数。
- 最后剩下的、没被划掉的数就是质数。
这个方法很直观,但有一个小缺点:有些合数会被重复划掉多次。比如 6 既是 2 的倍数又是 3 的倍数,会被划两次。虽然不影响结果,但浪费了一点时间。埃氏筛的时间复杂度是 O(N log log N),对于 N = 10⁷ 已经非常快。
2. 线性筛(欧拉筛)——每个合数只被标记一次,更快更省事
线性筛改进了埃氏筛,保证每个合数只被它的最小质因子标记一次,时间复杂度降到 O(N)。怎么做到的呢?
- 维护一个质数列表 primes。
- 从 i = 2 遍历到 N:
- 如果 i 是质数(没被标记过),把它加入 primes。
- 然后遍历质数列表中的每个质数 p:
- 如果 i * p > N,停止。
- 标记 i * p 为合数,并记录它的最小质因子为 p。
- 如果 p 能整除 i(即 p 是 i 的因子),则 break(重要!这保证每个合数只被最小质因子标记一次)。
为什么这一步能保证“只被最小质因子标记”?因为当 p 能整除 i 时,对于更大的质数 p',i * p' 的最小质因子一定是 p(因为 p 比 p' 小),所以后面不应该再由 p' 来标记。提前 break 就避免了重复。
3. 最小质因子数组(SPF)——分解的“超级快捷键”
在线性筛过程中,我们可以同时记录每个数的最小质因子,存到一个数组 spf 里:
- 如果 i 是质数,spf[i] = i(因为质数的最小质因子就是它自己)。
- 如果 i 是合数,在线性筛标记它时,spf[i * p] = p(p 是 i 乘以的那个质数,也是这个合数的最小质因子)。
有了 spf,分解一个数 x 就变成了一件简单到不能再简单的事情:
while x > 1:
p = spf[x] // 拿到当前数的最小质因子
记录 p
x = x / p // 除掉这个因子
由于每次我们拿到的都是当前数的最小质因子,而最小质因子已经提前存好了,所以连试除都不需要,直接除法就行。整个过程的步数等于 x 的质因子个数(含重复),也就是 O(log x) 级别。对于 10⁷ 以内的数,几乎瞬间完成。
三、编程实现:把数学变成代码
下面我们用 C++ 和 Python 分别实现一个完整的例子:先用线性筛预处理出 1 到 100 万的 spf 数组,然后读入一个数,快速分解并打印出来。
代码中的变量名都用简短英文单词,每行变量定义都写了中文注释,方便你理解。
C++ 实现
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 10000000; // 预处理的数范围,根据需求调整
vector<int> primes; // 存储质数
vector<int> spf; // 最小质因子数组,spf[i]表示i的最小质因子
// 线性筛,同时计算spf
void linear_sieve(int n) {
spf.resize(n + 1); // 让spf有n+1个元素,索引从0到n
for (int i = 2; i <= n; i++) {
if (spf[i] == 0) { // 如果spf[i]==0,说明i未被标记,i是质数
spf[i] = i; // 质数的最小质因子就是它自己
primes.push_back(i); // 把i加入质数列表
}
// 遍历质数列表,用每个质数与i相乘
for (int p : primes) {
if (p > spf[i] || i * p > n) // 保证每个合数只被最小质因子标记
break;
spf[i * p] = p; // i*p的最小质因子设为p
}
}
}
// 利用spf分解质因数,返回质因子列表(包含重复)
vector<int> fast_factorize(int x) {
vector<int> factors;
while (x > 1) {
int p = spf[x]; // 获取x的最小质因子
factors.push_back(p); // 记录这个因子
x /= p; // 除掉这个因子
}
return factors;
}
int main() {
// 预处理[1, 1000000]范围内的spf
linear_sieve(1000000);
int num;
cout << "请输入一个整数(<=1000000): ";
cin >> num;
if (num <= 1) {
cout << "输入必须大于1" << endl;
return 0;
}
vector<int> res = fast_factorize(num);
cout << num << " 的质因数分解: ";
for (size_t i = 0; i < res.size(); i++) {
cout << res[i];
if (i != res.size() - 1) cout << " * ";
}
cout << endl;
// 验证:将分解结果乘回去看看是否等于原数
long long product = 1;
for (int f : res) product *= f;
cout << "验证: " << product << (product == num ? " (正确)" : " (错误)") << endl;
return 0;
}
Python 实现
def linear_sieve(n):
"""
线性筛,返回 (质数列表, spf数组)
spf[i] 表示i的最小质因子,若i是质数则spf[i]=i
"""
primes = [] # 存储质数
spf = [0] * (n + 1) # spf数组,初始为0表示未被标记
for i in range(2, n + 1):
if spf[i] == 0: # i是质数
spf[i] = i
primes.append(i) # 加入质数列表
# 用每个质数与i相乘
for p in primes:
if p > spf[i] or i * p > n:
break
spf[i * p] = p # 标记合数的最小质因子
return primes, spf
def fast_factorize(x, spf):
"""利用spf数组快速进行质因数分解,返回因子列表"""
factors = []
while x > 1:
p = spf[x] # 获取x的最小质因子
factors.append(p) # 记录
x //= p # 除掉
return factors
def main():
# 预处理100万以内的spf
n_max = 1000000
primes, spf = linear_sieve(n_max)
try:
num = int(input(f"请输入一个整数(<= {n_max}): "))
if num <= 1:
print("输入必须大于1")
return
factors = fast_factorize(num, spf)
print(f"{num} 的质因数分解: {' * '.join(map(str, factors))}")
# 验证
prod = 1
for f in factors:
prod *= f
print(f"验证: {prod} {'(正确)' if prod == num else '(错误)'}")
except ValueError:
print("请输入有效整数")
if __name__ == "__main__":
main()
四、新手容易犯的 3 个错误
-
忘记处理 n = 1
1 不是质数也不是合数,spf[1] 没有意义。在分解函数中,如果输入是 1 或更小的数,应该直接返回空列表或报错,否则 while 循环永远不会结束(因为 spf[1] 未定义)。 -
线性筛中 break 条件写错
很多初学者把if (p > spf[i] || i * p > n) break;写成if (i * p > n) break;,这样会导致某些合数被重复标记(比如 12 会被 2×6 和 3×4 两次标记),效率下降,但通常不影响正确性。不过为了“线性”的名号,一定要加上p > spf[i]的判断。 -
数组大小没开够
如果要分解的数 x 大于预处理的最大值 N,那么 spf[x] 会访问越界。解决办法是确保所有待分解的数都 ≤ N,或者用更大的 N 预处理。如果 N = 10⁷,spf 数组 int 类型大约占 40 MB,对于现代电脑还算可以,但 N = 10⁸ 就接近 400 MB,可能内存不够了。
五、完整示例:分解 123456
假设我们预处理了 1 到 1 000 000 的 spf,输入 123456:
- spf[123456] = 2 → 记录 2,x = 61728
- spf[61728] = 2 → 记录 2,x = 30864
- spf[30864] = 2 → 记录 2,x = 15432
- spf[15432] = 2 → 记录 2,x = 7716
- spf[7716] = 2 → 记录 2,x = 3858
- spf[3858] = 2 → 记录 2,x = 1929
- spf[1929] = 3 → 记录 3,x = 643
- spf[643] = 643(质数) → 记录 643,x = 1,结束。
所以 123456 = 2⁶ × 3 × 643。代码运行后会输出:123456 的质因数分解: 2 * 2 * 2 * 2 * 2 * 2 * 3 * 643。
六、进阶:用筛法分解区间内的所有数(区间筛)
有时你需要分解一个区间 [L, R] 内的所有数,比如 L = 10¹², R = 10¹² + 10⁶,直接开一个 10¹² 的数组显然不可能。但我们可以只筛出 √R 以内的质数(约 10⁶),然后用这些质数去“标记”区间内的合数,并记录每个合数的最小质因子。这种方法叫做区间筛,在编程竞赛中很常见。具体思路是:
- 先筛出 1 到 √R 的所有质数。
- 对于每个质数 p,找到它在 [L, R] 内的第一个倍数,然后开始标记,同时记录最小质因子。
- 最后,区间内那些没有被标记的数就是质数,它们的质因子就是它们自己。
区间筛可以让你用 O(√R log log R + (R-L) log log R) 的时间分解一大段区间内的所有数,非常高效。有兴趣的话可以自己动手实现一下。
七、练习
-
修改代码,输出每个质因子的指数(比如 12 = 2² × 3,而不是 2 * 2 * 3)。
提示:在 fast_factorize 中,当连续获取相同因子时,可以计数。建议用字典或 pair 保存(质因子,指数)。 -
写一个程序,读入整数 N,输出所有 2~N 的数的质因数分解,每行一个数。
提示:先预处理 spf,然后对每个数调用 fast_factorize。注意分解结果可以按格式打印,比如6 = 2 * 3。 -
思考题:如果 spf 数组使用
short类型(2 字节)存储,最大能支持多大的 N?有什么风险?
提示:short能表示的最大值是 65535,所以 N 只能 ≤ 65535。如果 N 更大,spf 值会溢出(比如 65536 的最小质因子是 2,但 65536 超出了 short 范围,会被截断成负数或错误值)。所以用 short 要谨慎,通常用int或unsigned int更安全。
八、相关指引
- 如果你对埃氏筛和线性筛的原理还不太清楚,可以搜索“埃拉托斯特尼筛法”和“欧拉筛”的详细教程。
- 筛法预处理不仅用于质因数分解,还常用于求欧拉函数 φ(n)、莫比乌斯函数 μ(n) 等数论函数。
- 如果遇到需要分解 10¹⁸ 级别的大数(比如 RSA 密码中的大数),筛法就无能为力了,这时需要学习 Miller-Rabin 素性测试 和 Pollard Rho 质因数分解算法。它们不需要预处理,但速度也很快。
希望这篇文章能帮你理解“先造工具再干活”的筛法预处理思想。以后遇到需要大量分解质因数的题目,记得先用线性筛把 SPF 数组准备好,然后就能以闪电般的速度解决问题啦!
例题精讲
在使用线性筛预处理得到每个数的最小质因子(lpf)数组后,要分解一个正整数n(n ≤ 10^7)的质因数,以下哪种做法最合理?
在埃拉托斯特尼筛法中,我们只需从2到√n的每个数进行标记,就可以得到所有不超过n的质数。
已知数组 lpf[0..MAXN] 已通过线性筛预处理,lpf[i] 表示 i 的最小质因子。以下函数 decompose 用于打印 n 的质因数分解(如 12 = 2^2 * 3^1)。请填空完成代码。
void decompose(int n) {
while (n > 1) {
int p = lpf[n];
int cnt = 0;
while (___) {
___;
___;
}
printf("%d^%d ", p, cnt);
}
}关于线性筛(欧拉筛)和埃拉托斯特尼筛法的区别,以下哪个说法是正确的?
在使用筛法预处理得到最小质因子数组后,快速质因数分解的时间复杂度是O(√n)。