线性筛法:为什么 i % p == 0 时要 break?
前言
网上关于线性筛法的教程并不少。大多数教程都会强调,代码中的
if (i % p == 0) break;
可以保证每个合数只被筛一次。然而在我学习线性筛法时,即使记住了这个结论,也很难真正理解:为什么这样一句判断就能避免重复标记?
后来,我逐渐想清楚了这条语句背后的原理,因此想结合自己的理解,对线性筛法作一次较为完整的说明。本文会将重点放在上述 break 语句上。
一、回顾埃氏筛法
埃拉托斯特尼筛法(以下简称“埃氏筛”)基于一个简单的事实:
一个质数的正整数倍(不包括它本身)一定是合数。
我们可以使用 vis 数组记录一个数是否已经被判定为合数:若 vis[x] = true,则说明
一种基本实现如下:
vector<bool> vis(n + 1, false);
vis[1] = 1; // 1不为素数
for (int i = 2; i <= n; ++i) {
if (!vis[i]) {
for (int j = 2 * i; j <= n; j += i) {
vis[j] = true;
}
}
}
假设当前枚举到 vis[i] = false,说明不存在小于
但是,这种做法可能会多次标记同一个合数。
例如:
当
二、线性筛法的目标
线性筛法(也称欧拉筛)的核心目标是:
让每个合数只被它的最小质因子标记一次。
例如,
将其标记,而不再用质因子
设合数
那么
线性筛规定:
为了实现这一目标,除了 vis 数组之外,还需要使用 primes 数组,按照从小到大的顺序保存目前已经找到的所有质数。
三、代码模板
vector<bool> vis(n + 1, false);
vector<int> primes;
vis[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!vis[i]) {
primes.push_back(i);
}
for (int p : primes) {
if (i * p > n) break; // 处理边界
vis[i * p] = true; // 重点 1:标记合数 i * p
if (i % p == 0) break; // 重点 2:p 能整除 i 时停止
}
}
四、vis[i * p] = true 在做什么?
在埃氏筛中,当我们找到一个质数
在线性筛中,外层循环的 primes 中的质数
例如,当 primes = [2, 3] 时,会依次标记
随着外层循环继续向后移动,其他合数也会以类似的形式被标记。不过,内层循环并不会无条件枚举所有已知质数,而是在第一次遇到能够整除
五、为什么 i % p == 0 时必须停止?
假设 primes 中的质数依次为
当内层循环第一次枚举到满足
的质数 primes 按照从小到大的顺序保存质数,所以
此时,代码已经标记了
这个数的最小质因子仍然是
接下来考虑:如果此处不执行 break,而是继续枚举下一个质数
因为
事实上,这个数可以改写为
由于
外层循环以后一定会枚举到
如果当前不停止,那么这个合数会先被 i % p == 0 时,必须结束内层循环。
一个具体例子
假设当前
当
由于 6 % 2 == 0,内层循环在此停止。
如果不停止并继续枚举
但是,
被标记,而不应该在 break 正是避免了这一次重复操作。
六、为什么 p 不能整除 i 时可以继续?
设
由于 i % p != 0 时使用
例如,当
其中
其中
其中
当枚举到 5 % 5 == 0,因此停止。如果继续枚举
因此,可以这样理解:
在遇到能够整除
i 的第一个质数之前,当前质数p 是i\times p 的最小质因子;遇到之后,更大的质数都不再是相应乘积的最小质因子,所以必须停止。
七、为什么每个合数一定会被标记,而且只被标记一次?
上面的分析说明了为什么需要 break。下面再从整体上说明线性筛的正确性。
任取一个合数
于是
由于
所以,当外层循环枚举到
vis[i * p] = true;
从而标记
另一方面,
也是唯一确定的。因此,每个合数都会被标记,并且只会由自己的最小质因子标记一次。
八、时间复杂度
在线性筛中,每次真正执行
vis[i * p] = true;
都会标记一个此前不会以其他方式重复标记的合数。因为不超过
总结
线性筛最核心的规则是:
每个合数只由它的最小质因子标记一次。
代码中的
if (i % p == 0) break;
表示当前已经枚举到了
因此,break 并不是一个单纯的代码技巧,而是保证“每个合数只由最小质因子筛一次”的关键。