- bitworld 的博客
【CSP-J初赛】筛法求质数
- 2025-7-28 16:09:31 @
筛法
1. 什么是筛法
筛法(Sieve Method)是一类在计算理论,尤其是数论中,用于高效找出一定范围内所有满足特定性质的数的算法。其核心思想类似于“筛选”:从一个包含所有可能候选数的集合开始,通过一系列系统性的排除操作,将不满足性质的数(通常是合数)剔除,最终剩下的就是我们所需要的数(通常是质数)。
筛法最经典和最基础的应用是寻找范围内的所有质数,即质数筛。我们将从最古老的埃拉托斯特尼筛法(Sieve of Eratosthenes)讲起,并逐步深入到更高效的线性筛法(Linear Sieve),最后探讨筛法思想的扩展应用。
2. 埃拉托斯特尼筛法(Sieve of Eratosthenes)
埃拉托斯特尼筛法,简称埃氏筛,由古希腊数学家埃拉托斯特尼在公元前3世纪提出,是第一个也是最著名的质数筛选算法。
2.1 核心思想
埃氏筛的原理非常直观:
- 创建一个布尔数组 ,初始时将所有大于等于 的数都标记为质数。
- 从最小的质数 开始,将所有 的倍数()标记为合数。
- 找到下一个未被标记的数,这个数一定是质数(因为如果它是合数,那么它一定有一个比它更小的质因子,而这个质因子在之前的步骤中就应该已经把它标记为合数了)。这个数是 。
- 将所有 的倍数()标记为合数。
- 重复这个过程,找到下一个未被标记的质数 ,然后将所有 的倍数标记为合数。
- 这个过程持续到我们检查的质数 的平方 大于要筛选的范围上限 为止。因为如果一个合数 的最小质因子大于 ,那么 至少是两个大于 的数的乘积,其结果必然大于 ,这超出了我们的范围。
2.2 过程
假设我们需要找出 到 之间的所有质数。
-
初始状态,我们认为 到 都是质数。
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
-
找到第一个质数 ,划掉它的所有倍数()。
2 3
45
67
89
1011
1213
1415
1617
1819
20剩下的可能是质数的数:2 3 5 7 9 11 13 15 17 19
-
找到下一个质数 ,划掉它的所有倍数()。其中一些已经被划掉了。
2 3 5 7
911 13
1517 19
剩下的可能是质数的数:2 3 5 7 11 13 17 19
-
找到下一个质数 。它的平方是 ,已经大于 ,所以我们停止筛选。
最终,所有未被划掉的数就是 以内的质数:。
2.3 代码实现
基础版本
题意概括: 找出并打印 到 之间的所有质数。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1000005;
bool isp[N]; // is prime? isp[i]为true代表i是质数
void sieve(int n) {
memset(isp, 1, sizeof(isp)); // 初始化,假设所有数都是质数
isp[0] = isp[1] = 0; // 0和1不是质数
for (int i = 2; i <= n; ++i) {
if (isp[i]) { // 如果i是质数
// 将i的倍数标记为合数
// j可以从2*i开始,也可以从i*i开始,后者是优化
for (int j = 2 * i; j <= n; j += i) {
isp[j] = 0;
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
sieve(n);
for (int i = 2; i <= n; ++i) {
if (isp[i]) {
cout << i << " ";
}
}
cout << endl;
return 0;
}
- 代码解释:
isp
数组用于标记。sieve
函数中,外层循环遍历 到 的所有数。如果发现isp[i]
仍然为true
,说明 是一个质数,因为它没有被任何比它小的质数筛掉。然后,内层循环就将 的所有倍数标记为合数。 - 时间复杂度分析:外层循环遍历每个数,内层循环对每个质数 会执行 次操作。所以总的操作次数近似为 。根据梅滕斯第二定理,质数倒数之和近似于 。所以,埃氏筛的时间复杂度为 。这是一个非常接近线性的高效复杂度。
- 空间复杂度分析:我们需要一个大小为 的布尔数组来记录每个数是否为质数,所以空间复杂度为 。
优化版本
我们可以对基础版本做一些优化。
-
内层循环起点:对于一个质数 ,它的倍数 其实在之前的步骤中已经被 等更小的质数筛过了。例如,筛 的倍数时, 已经在筛 的时候被处理。因此,我们可以让内层循环直接从 开始。这是因为任何一个合数 (不妨设 ),它必然有一个小于等于 的质因子。当我们的筛子进行到 时, 就已经被筛掉了。所以对于质数 ,小于 的 的倍数,都已经被更小的质数处理过。
-
外层循环终点:同理,外层循环的 只需要遍历到 即可。因为如果一个数 是合数,它的最小质因子一定小于等于 。如果我们只对小于等于 的质数进行筛选,那么所有 以内的合数都会被它们小于等于 的质因子筛掉。
题意概括: 优化地找出并打印 到 之间的所有质数。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1000005;
bool isp[N];
void sieve(int n) {
memset(isp, 1, sizeof(isp));
isp[0] = isp[1] = 0;
for (int i = 2; i * i <= n; ++i) { // 优化1:外层循环到sqrt(n)
if (isp[i]) {
// 优化2:内层循环从i*i开始
for (ll j = (ll)i * i; j <= n; j += i) {
isp[j] = 0;
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
sieve(n);
for (int i = 2; i <= n; ++i) {
if (isp[i]) {
cout << i << " ";
}
}
cout << endl;
return 0;
}
- 代码解释:注意
j
的类型为ll
(long long),防止i * i
溢出。这个优化后的版本在实践中效率更高,但其时间复杂度的理论上界仍然是 。
2.4 埃氏筛的局限性
埃氏筛虽然高效,但它存在一个问题:一个合数可能会被它的多个质因子重复筛选。
例如,数字 :
- 在筛质数 时, 被标记为合数。
- 在筛质数 时, 又被标记了一次。
这种重复操作虽然不影响结果的正确性,但却是时间上的浪费。如果我们能让每个合数都只被筛选一次,能否做到严格的 复杂度呢?答案是肯定的,这就是线性筛法。
3. 线性筛法(Linear Sieve)
线性筛法,也称为欧拉筛(Euler's Sieve),是一种能在严格线性时间内,即 时间内找出所有质数的算法。它的核心思想是确保每个合数都只被其最小质因子(Smallest Prime Factor, SPF)筛选一次。
3.1 核心思想与原理
为了实现“只被最小质因子筛选”,线性筛在遍历每个数 的时候,不仅仅是标记 的倍数,而是利用已经找到的质数来构造合数。
具体步骤如下:
- 我们需要一个数组 来存储已经找到的质数,一个计数器 记录质数个数。同样,需要一个布尔数组 来标记。
- 我们从 开始遍历到 。
- 对于当前遍历到的数 :
a. 如果
isp[i]
仍然为true
(或者在某些实现中用 表示未被标记),说明 是一个质数。我们将它存入质数数组 中,即p[cnt++] = i;
。 b. 关键步骤:不论 是质数还是合数,我们都要用它去与已经找到的质数 相乘,来筛选掉新的合数i * p[j]
。 c. 我们遍历质数数组 ,对于每个质数p[j]
,将i * p[j]
标记为合数。 d. 终止条件:这个遍历质数数组的过程需要一个终止条件,以确保“只被最小质因子筛选”。这个条件是:如果 是 的倍数,即i % p[j] == 0
,则立刻停止内层循环。
3.2 原理证明:为何 i % p[j] == 0
时要停止?
这是线性筛的精髓所在。让我们来分析一下。
我们正在用数 和质数 来筛选合数 。我们希望这个合数是被它的最小质因子筛掉的。
-
Case 1:
i % p[j] != 0
在这种情况下, 是比 的所有质因子都要小的。因为我们是按顺序遍历质数数组 的,所以 是我们当前能用的最小的质数之一。由于 不能被 整除,所以 的最小质因子必然大于 。因此,合数 的最小质因子就是 。这符合我们的要求:合数 被其最小质因子 筛掉。 -
Case 2:
i % p[j] == 0
在这种情况下, 本身就是 的倍数。这意味着 是 的一个质因子,而且由于我们是按从小到大的顺序遍历质数 的,所以 一定是 的最小质因子。此时,我们筛掉了合数 。它的最小质因子是 ,这没有问题。
但是,如果我们继续内层循环,用下一个质数 去和 相乘,会得到合数 。让我们来分析这个新合数。
因为 ( 是某个整数),所以 。
这个新合数 的最小质因子是什么?由于 ,所以它的最小质因子是 ,而不是 。
这意味着,如果我们继续循环,合数 将会被 筛掉。但我们希望它被它的最小质因子 筛掉。这个数应该在什么时候被筛掉呢?它应该在遍历到数 时,与质数 相乘时被筛掉,即 。
所以,为了保证每个合数只被其最小质因子筛掉,一旦我们发现
i % p[j] == 0
,就必须break
。因为后续用 和更大的质数 构造出的合数,它们的最小质因子都会是 ,而不是 。这些合数应该留给后面某个 (其最小质因子大于 )去处理。
通过这个 break
条件,我们完美地保证了每个合数 都是以 的形式被筛掉的,其中 是 的最小质因子。
3.3 代码实现
题意概括: 使用线性筛法,找出并打印 到 之间的所有质数,并记录每个数的最小质因子。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 10000005;
bool isp[N]; // is prime? 0代表质数,1代表合数
int p[N / 10]; // 存放质数
int spf[N]; // smallest prime factor
int cnt; // 质数计数
void sieve(int n) {
memset(isp, 0, sizeof(isp));
isp[0] = isp[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!isp[i]) { // i是质数
p[cnt++] = i;
spf[i] = i; // 质数的最小质因子是它自己
}
// 遍历已找到的质数来筛合数
// i * p[j] 是要筛掉的合数
for (int j = 0; j < cnt && (ll)i * p[j] <= n; ++j) {
int np = p[j]; // 当前质数
isp[i * np] = 1; // 标记为合数
spf[i * np] = np; // i*np的最小质因子是np
if (i % np == 0) { // 关键:i的最小质因子是np
break; // 保证每个合数只被最小质因子筛一次
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
sieve(n);
for (int i = 0; i < cnt; ++i) {
cout << p[i] << " ";
}
cout << endl;
return 0;
}
- 代码解释:
isp
数组的含义与埃氏筛相反,0
代表未被筛(质数),1
代表已被筛(合数)。p
数组用来存储找到的质数,cnt
是当前质数的数量。spf
数组(Smallest Prime Factor)记录每个数的最小质因子,这是线性筛的一个非常有用的副产品。- 外层循环遍历 到 。如果 是质数,就加入 数组。
- 内层循环用 和已知的质数 来构造合数。循环有两个退出条件:一是构造的合数
i * p[j]
超出了范围 ;二就是i % p[j] == 0
。
- 时间复杂度分析:外层循环是 。关键是内层循环。由于每个合数 都只会被它的最小质因子 在 时筛一次,所以内层循环的总执行次数等于 以内的合数个数。因此,总的时间复杂度是 。
- 空间复杂度分析:需要
isp
,p
,spf
三个数组。isp
和spf
是 , 的大小约为 (根据质数定理),所以总空间复杂度为 。
4. 筛法思想的扩展应用
线性筛的强大之处不仅在于求质数,它的核心思想——“每个数只被特定因子处理一次”——可以被推广用来求解很多数论函数的前缀信息,这类函数被称为积性函数。
4.1 积性函数简介
一个数论函数 ,如果对于任意两个互质的正整数 和 (即 ),都有 ,那么称 为积性函数。
如果对于任意两个正整数 (不要求互质),都有 ,则称其为完全积性函数。
常见的积性函数:
- 欧拉函数 :小于等于 的正整数中与 互质的数的数目。
- 莫比乌斯函数 :
- 若 含有平方因子(即能被某个 整除),。
- 若 是 个不同质数的乘积,。
- 约数个数函数 或 : 的正约数的个数。
- 约数和函数 : 的正约数的和。
4.2 线性筛求欧拉函数
欧拉函数的计算公式为: 若 是 的标准分解式,则
$$\varphi(n) = n \prod_{i=1}^{k} \left(1 - \frac{1}{p_i}\right) = n \frac{p_1-1}{p_1} \frac{p_2-1}{p_2} \dots \frac{p_k-1}{p_k} $$在线性筛的框架内,我们考虑如何计算 。设 是当前质数。
-
当 是质数时: 对于一个质数 ,。这可以在筛到质数时直接计算。
-
当 是合数时,我们利用积性函数的性质。设 是 的最小质因子。
-
Case 1:
i % p_j != 0
此时 和 互质。根据积性函数的定义,$\varphi(i \times p_j) = \varphi(i) \times \varphi(p_j)$。 因为 是质数,。 所以,$\varphi(i \times p_j) = \varphi(i) \times (p_j - 1)$。 -
Case 2:
i % p_j == 0
此时 和 不互质, 是 的最小质因子。 这意味着 和 含有完全相同的质因子集合。 设 ,其中 。 则 。 根据 的计算公式: $\varphi(i) = i \prod_{p|i} (1-\frac{1}{p}) = (m \times p_j^k) \prod_{p|i} (1-\frac{1}{p})$ $\varphi(i \times p_j) = (i \times p_j) \prod_{p|i \times p_j} (1-\frac{1}{p})$ 因为 和 的质因子集合相同,所以它们的 部分是完全一样的。 $\frac{\varphi(i \times p_j)}{\varphi(i)} = \frac{i \times p_j}{i} = p_j$ 所以,。
-
总结一下递推关系:
- 如果 是质数,。
- 如果 ,$\varphi(i \cdot p_j) = \varphi(i) \cdot \varphi(p_j) = \varphi(i) \cdot (p_j - 1)$。
- 如果 ,。
代码实现
题意概括: 使用线性筛,计算并存储 到 所有数的欧拉函数值。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1000005;
bool isp[N];
int p[N / 10];
int phi[N];
int cnt;
void sieve(int n) {
memset(isp, 0, sizeof(isp));
isp[0] = isp[1] = 1;
phi[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!isp[i]) { // i是质数
p[cnt++] = i;
phi[i] = i - 1; // 质数p的phi(p) = p-1
}
for (int j = 0; j < cnt && (ll)i * p[j] <= n; ++j) {
int np = p[j];
isp[i * np] = 1;
if (i % np == 0) { // i的最小质因子是np
phi[i * np] = phi[i] * np;
break;
} else { // i和np互质
phi[i * np] = phi[i] * (np - 1);
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
sieve(n);
// 打印前20个欧拉函数值作为示例
for (int i = 1; i <= min(n, 20); ++i) {
cout << "phi(" << i << ") = " << phi[i] << endl;
}
return 0;
}
- 复杂度分析:与标准线性筛完全相同,时间复杂度和空间复杂度均为 。
4.3 线性筛求莫比乌斯函数
同样,我们可以在线性筛框架内递推计算 。
-
当 是质数时: 根据定义,质数 是 1 个不同质数的乘积,所以 。
-
当 是合数时:
-
Case 1:
i % p_j != 0
此时 和 互质。根据积性函数定义,。 因为 ,所以 。 -
Case 2:
i % p_j == 0
此时 含有质因子 。那么 必然含有平方因子 。 根据定义,任何含有平方因子的数,其莫比乌斯函数值为 。 所以,。
-
总结一下递推关系:
- 。
- 如果 是质数,。
- 如果 ,。
- 如果 ,。
代码实现
题意概括: 使用线性筛,计算并存储 到 所有数的莫比乌斯函数值。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1000005;
bool isp[N];
int p[N / 10];
int mu[N];
int cnt;
void sieve(int n) {
memset(isp, 0, sizeof(isp));
isp[0] = isp[1] = 1;
mu[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!isp[i]) { // i是质数
p[cnt++] = i;
mu[i] = -1; // mu(p) = -1
}
for (int j = 0; j < cnt && (ll)i * p[j] <= n; ++j) {
int np = p[j];
isp[i * np] = 1;
if (i % np == 0) { // i的最小质因子是np
mu[i * np] = 0; // 含有平方因子
break;
} else { // i和np互质
mu[i * np] = -mu[i];
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
sieve(n);
// 打印前20个莫比乌斯函数值作为示例
for (int i = 1; i <= min(n, 20); ++i) {
cout << "mu(" << i << ") = " << mu[i] << endl;
}
return 0;
}
- 复杂度分析:与标准线性筛完全相同,时间复杂度和空间复杂度均为 。
5. 例题
5.1 选择题
-
使用埃氏筛筛选出 以内的所有质数,数字 会被哪些质数筛掉? A. 2 B. 3 C. 5 D. 2, 3, 5
解析: 在标准的埃氏筛法中(未经优化的版本),一个合数会被它的所有质因子筛选。 的质因子为 。
- 当外层循环到质数 时,会标记所有 的倍数,因此 会被标记。
- 当外层循环到质数 时,会标记所有 的倍数,因此 会被标记。
- 当外层循环到质数 时,会标记所有 的倍数,因此 会被标记。 因此, 会被其所有质因子 筛掉。 答案:D
-
线性筛法的时间复杂度是? A. B. C. D.
解析: 线性筛法的核心思想是确保每个合数都只被其最小质因子筛选一次。由于每个数只被访问常数次,总的时间复杂度是线性的。 答案:C
-
在线性筛算法中,对于任意一个合数 ,它被标记为合数时,是基于以下哪种分解? A. ,其中 是 的任意一个质因子 B. ,其中 是 的最大质因子 C. ,其中 是 的最小质因子 D. ,其中 均为质数
解析: 线性筛的关键代码
if (i % p[j] == 0) break;
保证了当程序执行v[i * p[j]] = 1;
来标记合数时,p[j]
一定是合数i * p[j]
的最小质因子。 答案:C -
下列哪个函数不是积性函数? A. 欧拉函数 B. 莫比乌斯函数 C. 恒等函数 D. 最小质因子函数
解析: 积性函数的定义是:对于任意两个互质的正整数 ,有 。 A, B, C 均为常见的积性函数。 D: 最小质因子函数 (Smallest Prime Factor)。我们可以用一个反例来检验。令 ,它们互质。 , 。则 。 但是 , 。 因为 ,所以 不是积性函数。 答案:D
5.2 判断题
-
在经过优化的埃氏筛中(内层循环从 开始),合数 会被质数 筛掉。 ( √ )
解析: 优化的埃氏筛,外层循环遍历到质数 时,内层循环从 开始标记。当外层循环 时,内层循环会标记 。因此, 会被质数 筛掉。 答案:√
-
在线性筛中,当处理到 时,内层循环会因为
12 % 2 == 0
而在检查完质数 后立即break
。 ( √ )解析: 当外层循环进行到 时,已找到的质数列表
p
为 。内层循环从最小的质数p[0]=2
开始。j=0
,p[0]=2
。程序标记合数12 * 2 = 24
。- 随后检查
if (i % p[j] == 0)
,即if (12 % 2 == 0)
。该条件成立。 - 执行
break
,内层循环终止。 因此,程序不会再用质数 去乘 。 答案:√
-
任何积性函数都可以在 的时间内用线性筛预处理出来。 ( × )
解析: 该说法是错误的。虽然许多常见的积性函数(如欧拉函数 、莫比乌斯函数 、约数个数函数 )都可以用线性筛高效计算,但这并非普遍规律。能够使用线性筛求解一个积性函数 的前提是,我们必须能够高效地(通常是 时间)通过 和 的值推导出 的值(需要分别处理 与 互质和不互质两种情况)。对于某些复杂的积性函数,这种递推关系可能不存在,或者计算非常耗时,导致无法实现线性时间复杂度的预处理。 答案:×
5.3 程序阅读题
题目:阅读下面的C++代码,并回答问题。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 100;
int p[N], f[N];
bool v[N];
int cnt;
void solve(int n) {
f[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!v[i]) {
v[i] = 1;
p[cnt++] = i;
f[i] = 2;
}
for (int j = 0; j < cnt && (ll)i * p[j] <= n; ++j) {
v[i * p[j]] = 1;
if (i % p[j] == 0) {
// some logic here
break;
} else {
f[i * p[j]] = f[i] * f[p[j]];
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
solve(50);
cout << f[12] << endl;
return 0;
}
- 该程序实现的算法框架是?
- 变量
f[i]
试图存储的是数字i
的什么数论函数值? - 为了让程序正确计算该函数,
// some logic here
处应该填入什么代码? main
函数在当前代码下(即// some logic here
为空)最终会输出什么?
解析:
-
算法框架: 代码具有
for (int i...) { if (!v[i]) {...} for (int j...) {... if(i%p[j]==0) break;} }
的标志性结构,这毫无疑问是线性筛(或称欧拉筛)的算法框架。 -
函数值: 我们来分析
f
函数的性质:- 当
i
是一个质数p
时,代码设置f[i] = 2
。 - 当
i
和质数p[j]
互质时 (i % p[j] != 0
),代码执行f[i * p[j]] = f[i] * f[p[j]]
。这正是积性函数的定义。 一个在质数p
处取值为 的积性函数,最常见的就是约数个数函数 (因为一个质数p
只有 和p
两个约数)。因此,可以推断该程序试图计算并存储每个数字的约数个数。
- 当
-
补全代码: 原代码的主要问题在于
if (i % p[j] == 0)
的分支是空的。在这种情况下, 的值无法仅从 和 推导出来,必须知道 中最小质因子 的指数。 标准的线性筛求约数个数需要引入一个辅助数组,我们称之为g[i]
,记录 的最小质因子 的指数加一。 完整的递推逻辑如下:- 当
i
是质数时:f[i] = 2
,g[i] = 2
。 - 当
i % p[j] != 0
时 (互质):f[i * p[j]] = f[i] * f[p[j]] = f[i] * 2
。此时i * p[j]
的最小质因子是p[j]
,指数为1,所以g[i * p[j]] = 2
。 - 当
i % p[j] == 0
时 (不互质): 设 。 相比 ,只是最小质因子 的指数加了1。所以 $d(i \times p_j) = \frac{d(i)}{g(i)} \times (g(i)+1)$。 所以// some logic here
应该替换为:
// 假设 g[] 是辅助数组 f[i * p[j]] = f[i] / g[i] * (g[i] + 1); g[i * p[j]] = g[i] + 1;
- 当
-
main
函数输出: 在未经修改的原始代码中,// some logic here
是空的。我们来追踪f[12]
的计算过程:f[1]
被初始化为1
。f[2]=2
,f[3]=2
,f[5]=2
, ...- 当
i=2, j=0, p[j]=2
时,i % p[j] == 0
成立,执行break
。f[4]
的值从未被计算。 - 当
i=3, j=0, p[j]=2
时,i % p[j] != 0
成立,计算f[6] = f[3] * f[2] = 2 * 2 = 4
。 - 为了计算
f[12]
,程序需要执行到i=6, j=0, p[j]=2
。此时i % p[j] == 0
成立,执行break
,f[12]
未被计算。或者执行到i=4
,但f[4]
根本没有值。 由于f
是一个全局数组,未被赋值的元素会被默认初始化为0
。因为代码逻辑的缺陷,f[12]
始终不会被赋值。因此,程序最终会输出其初始值。 输出:0
如果代码被修正,
f[12]
的值会被正确计算出来:。
5.4 程序补全题
题意概括: 下面的代码使用线性筛计算 到 所有数的最小质因子 spf[i]
。请补全 ___1___
, ___2___
, ___3___
, ___4___
四处空白。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1000005;
int p[N / 10];
int spf[N];
bool v[N];
int cnt;
void sieve(int n) {
v[0] = v[1] = 1;
for (int i = 2; i <= n; i++) {
if (!v[i]) {
p[cnt++] = i;
___1___; // 当i是质数时
}
for (int j = 0; j < cnt && (ll)i * p[j] <= n; j++) {
v[___2___] = 1;
spf[i * p[j]] = ___3___;
if (i % p[j] == 0) {
___4___;
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
sieve(1000000);
cout << "spf[99] = " << spf[99] << endl;
cout << "spf[100] = " << spf[100] << endl;
return 0;
}
解析与答案:
___1___
: 在if (!v[i])
块中,i
被确定为一个质数。一个质数的最小质因子就是它自身。所以此处应填spf[i] = i;
。___2___
: 内层循环的目的是标记合数。当前正在处理的合数是i
和质数p[j]
的乘积,所以需要标记v[i * p[j]]
。此处应填i * p[j]
。___3___
: 线性筛保证了任何合数N = i * p[j]
都是被其最小质因子p[j]
筛掉的。所以i * p[j]
的最小质因子就是p[j]
。此处应填p[j]
。___4___
: 这是线性筛的核心机制。当i
是p[j]
的倍数时,意味着p[j]
是i
的最小质因子。此时必须break
循环,以防止后续更大的质数p[j+1]
去筛选i
,从而保证每个合数只被其最小质因子筛一次。此处应填break;
。
补全后代码:
___1___
: spf[i] = i;
___2___
: i * p[j]
___3___
: p[j]
___4___
: break;
main函数输出:
- 对于 ,其质因数分解为 。它的最小质因子是 。所以
spf[99]
的值是3
。 - 对于 ,其质因数分解为 。它的最小质因子是 。所以
spf[100]
的值是2
。
埃氏筛 ():思想简单直观,易于实现。通过标记质数的倍数为合数来筛选。虽然存在重复标记,但效率已经非常高,足以应对大部分百万级别数据规模的质数筛选问题。
线性筛 ():通过精巧的设计,保证每个合数仅被其最小质因子筛选一次,达到了理论最优的线性时间复杂度。其核心思想不仅能用于求质数,更能推广到在线性时间内预处理各种积性函数(如欧拉函数、莫比乌斯函数等),是解决更复杂数论问题的基石。