在遇到数学问题时我们经常需要进行素数的筛选 而且我们不知道我们筛选出来的素数是第几位的素数下面我们将介绍一段线性筛选素数的代码以供大家学习
题目要求我们
# 【模板】线性筛素数
## 题目背景
本题已更新,从判断素数改为了查询第 k 小的素数
提示:如果你使用 `cin` 来读入,建议使用 `std::ios::sync_with_stdio(0)` 来加速。
## 题目描述
如题,给定一个范围 n,有 q个询问,每次输出第 k小的素数。
## 输入格式
第一行包含两个正整数 n,q,分别表示查询的范围和查询的个数。
接下来 q行每行一个正整数 k,表示查询第 k 小的素数。
## 输出格式
输出 q 行,每行一个正整数表示答案。
## 样例 #1
### 样例输入 #1
```
100 5
1
2
3
4
5
```
### 样例输出 #1
```
2
3
5
7
11
```
下面是示例code
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e8 + 5;
bool is_prime[MAXN];
vector<int> primes;
void sieve(int n) {
memset(is_prime, true, sizeof(is_prime));
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
primes.push_back(i);
}
for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) {
is_prime[i * primes[j]] = false;
if (i % primes[j] == 0) {
break;
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
sieve(n);
while (q--) {
int k;
cin >> k;
if (k <= primes.size()) {
cout << primes[k - 1] << endl;
} else {
cout << "Invalid query" << endl;
}
}
return 0;
}
代码需要我们实现的功能如下
即我们定义一个范围为100的筛查范围 以及可查询的5次次数
当我们在第二行输入时可选择要查找的第X个素数下一步即显示我们要查找到的素数
好了解代码的功能之后我们来学习一下代码如何实现
我们开始分析一下这段代码要实现的流程首先我们引入了作为我们的头文件及使用标准命名空间,避免每次调用标准库时都要加上std::前缀
#include <bits/stdc++.h>
using namespace std;
在C++中,#include <bits/stdc++.h>
是一个编译器特定的头文件,主要用于在线编程竞赛或快速原型开发。这个头文件包含了几乎所有标准库的头文件,因此它允许程序员在编写代码时不必显式地包含每一个需要的标准库头文件。
然而,值得注意的是,#include <bits/stdc++.h>
并不是一个标准的C++头文件,这意味着不同的编译器可能会有不同的支持程度,或者干脆不支持。特别地,一些编译器可能不包含这个头文件,或者即使包含,也可能不包含所有的标准库头文件。此外,由于它包含了大量的头文件,可能会导致编译时间变长,生成的代码体积变大,这对于生产环境的代码是不利的。
因此,尽管#include <bits/stdc++.h>
在快速原型开发或在线编程竞赛中可能很有用,但在实际的软件开发项目中,更推荐显式地包含所需要的头文件,这样可以提高代码的可读性和可移植性。
值得一提的是using namespace std;我们其实不是很提倡开发者使用命名空间,using namespace std;
这个语句是将std
(标准命名空间)中的所有名称都引入到当前的命名空间中,这样你就可以直接使用这些名称,而不必每次都加上std::
前缀。然而,这种做法也可能导致命名冲突,尤其是在大型项目中。因此,一些编码规范推荐避免在头文件或全局范围内使用using namespace std;
,而是在需要的函数或代码块内部使用。
接下来我们进入分析部分 我们需要一个函数来进行线性筛选素数这个功能我们不放在主函数中去做因为数据太过夯杂所以我们需要一个sieve函数来实现线性筛选素数的功能
主函数部分
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
sieve(n);
while (q--) {
int k;
cin >> k;
if (k <= primes.size()) {
cout << primes[k - 1] << endl;
} else {
cout << "Invalid query" << endl;
}
}
return 0;
}
我们使用了题目所给的
ios::sync_with_stdio(false);
来进行同步iostream与stdio库,加速输入输出
同时我们使用了
cin.tie(nullptr);
来解除cin与cout输出与输入流的绑定,进一步加入输入输出。
剩下的不难理解我们就以注释的方式来对主函数的功能进行分析了
int n, q; // 定义两个整数变量n和q
cin >> n >> q; // 从标准输入读取n和q的值
sieve(n); // 调用sieve函数筛选n以内的素数
while (q--) { // 循环q次
int k; // 定义整数变量k
cin >> k; // 从标准输入读取k的值
if (k <= primes.size()) { // 如果k小于等于primes向量的大小
cout << primes[k - 1] << endl; // 输出primes向量中第k个素数(因为数组下标从0开始,所以要减1)
} else { // 如果k大于primes向量的大小
cout << "Invalid query" << endl; // 输出错误信息
}
}
当然别忘了return 0;嘿嘿🤭
好的接下来进入本篇文章的重点!!!
就是实现sieve函数的实现啦敲黑板ing......
void sieve(int n) { // 定义一个名为sieve的函数,用于筛选n以内的素数
memset(is_prime, true, sizeof(is_prime)); // 使用memset函数将is_prime数组的所有元素初始化为true
is_prime[0] = is_prime[1] = false; // 将0和1标记为非素数
for (int i = 2; i <= n; ++i) { // 从2开始遍历到n
if (is_prime[i]) { // 如果i是素数
primes.push_back(i); // 将i加入到primes向量中
}
for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) { // 遍历primes向量中的每个素数,并与i相乘
is_prime[i * primes[j]] = false; // 将i与primes[j]的乘积标记为非素数
if (i % primes[j] == 0) { // 如果i能被primes[j]整除
break; // 跳出内层循环,因为此时i*primes[j+1]、i*primes[j+2]...都是合数,不需要再判断
}
}
}
}
上文中我们将is_prime定义成了一个bool类型的数组,用于标记每个数是否为素数并给它划定了一个范围如下:
const int MAXN = 1e8 + 5; // 定义一个常量MAXN,其值为1亿零5
bool is_prime[MAXN]; // 定义一个布尔类型的数组is_prime,用于标记每个数是否为素数
vector<int> primes; // 定义一个整数类型的向量primes,用于存储素数
我们要知道的是我们从小开始就知道我们....嗯对.....0和1不是素数嘛 你问我我也不知道,你要非要问的话我也可以给你讲一下
0和1不是素数的原因如下:
- 素数的定义:素数是大于1的自然数,且除了1和它本身以外不再有其他因数的数。素数在数论中有着重要的地位,它是两个更大的素数之间的自然数集合的“基本构建块”。
- 0的特性:0是一个特殊的数,它不能作为除数(因为任何数除以0都是未定义的),并且0没有正因数(除了它自身)。由于素数定义中明确要求数必须大于1,并且要有除了1和它本身以外的因数,所以0不满足这些条件,因此0不是素数。
- 1的特性:1是所有自然数的因数,但它只有一个正因数(即它自身)。由于素数定义中要求数除了1和它本身以外不再有其他因数,而1只有一个因数(即它自身),因此1不满足素数的定义。此外,由于素数定义中明确要求数必须大于1,1也不满足这一条件。
综上所述,0和1由于不符合素数的定义,所以它们不是素数。所以我们将在外层for循环中从2开始遍历到n,理解了吗小伙伴们^_^
ok啦ok我们接下来继续学习
首先呢我们利用memset函数将所有的在is_prime的数组中的所有元素初始化为true嗯也就是默认他们都是素数
if (is_prime[i]) { // 如果i是素数
primes.push_back(i); // 将i加入到primes向量中
}
下面我们只需要鉴别一下不是素数的数并把他们踹出去就行了是吧
for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) { // 遍历primes向量中的每个素数,并与i相乘
is_prime[i * primes[j]] = false; // 将i与primes[j]的乘积标记为非素数
if (i % primes[j] == 0) { // 如果i能被primes[j]整除
break; // 跳出内层循环,因为此时i*primes[j+1]、i*primes[j+2]...都是合数,不需要再判断
}
}
值得一提的是这个for
循环是埃拉托斯特尼筛法(Sieve of Eratosthenes)中的一部分,用于标记从2到n的所有合数。在这个循环中,i
是一个正在考虑的数(通常从2开始并逐渐增大),而primes
是一个存储所有已知素数的向量。
循环的条件有三个部分:
-
int j = 0;
:初始化内层循环的计数器j
,从0开始,这对应于primes
向量的第一个元素。 -
j < primes.size()
:确保j
没有超出primes
向量的范围。这是为了避免数组越界错误。 -
i * primes[j] <= n
:这个条件确保我们只标记那些小于或等于n
的数的合数性。这是因为我们只对从2到n的数感兴趣,所以没有必要标记更大的数。
循环体中的代码执行以下操作:
-
is_prime[i * primes[j]] = false;
:将i
与primes[j]
的乘积标记为非素数。这是因为任何素数的倍数(除了该素数本身)都是合数。 -
if (i % primes[j] == 0)
:这个条件检查i
是否可以被primes[j]
整除。如果i
能被primes[j]
整除,那么i
一定包含primes[j]
作为它的一个因数。 -
break;
:如果上述条件成立(即i
能被primes[j]
整除),则跳出内层循环。这是因为一旦我们找到一个数i
的素数因数(在这里是primes[j]
),我们就知道i
与primes[j]
之后的所有素数的乘积都将是合数。因此,没有必要继续检查i
与更大的素数的乘积。
这个循环通过标记合数来有效地生成素数列表。它利用了这样一个事实:任何合数都可以分解为素数的乘积。因此,一旦我们找到了一个数的素数因数,我们就可以快速地标记出所有由这个数和更大素数相乘得到的合数。
is_prime[i * primes[j]] = false; // 将i与primes[j]的乘积标记为非素数
这一段代码是实现算法的核心所在,我们知道如果已知一个素数那么它与另外一个数的乘积如果还在我们所遍历的范围之内的话那相乘所得的这个数一定不是素数,这段代码大大减少了我们计算机的工作量……嘿嘿为计算机高兴一下...
以上就是对线性筛素数的功能实现——当然还有很多地方或者很多解法可以优化我们要实现的功能,我们今天只是简单学习一下这类解法,若有不足之处请多多指正,感谢您的阅读,希望能给您的学习带来帮助