1.素数筛
2 判断素数常用方法
判断素数常用方法
给定一个正整数 n n n,请你判断它是否为素数。
输入格式
一个正整数 n n n ( 1 ≤ n ≤ 1 0 18 1 \le n \le 10^{18} 1≤n≤1018)。
输出格式
如果 n n n 是素数,输出 “Yes”,否则输出 “No”。
示例
输入
17
输出
Yes
题目解析:
#include <iostream> using namespace std; typedef long long ll; bool isPrime(ll n) { if (n <= 1) return false; for (ll i = 2; i * i <= n; i++) { if (n % i == 0) return false; } return true; } int main() { ll n; cin >> n; if (isPrime(n)) { cout << "Yes" << endl; } else { cout << "No" << endl; } return 0; }
我们首先定义了一个
isPrime
函数,用于判断一个数是否为素数。在
isPrime
函数中,我们首先排除了小于等于1的数,因为它们不是素数。if (n <= 1) return false;
然后,我们从2开始,逐个尝试去除以小于等于 n \sqrt{n} n 的每一个整数。这里我们用了
i * i <= n
来替代i <= sqrt(n)
,以避免使用浮点数。for (ll i = 2; i * i <= n; i++) { if (n % i == 0) return false; }
如果在这个过程中,我们发现 n n n 可以被 i i i 整除,那么 n n n 就不是素数,我们返回
false
。如果我们尝试了所有可能的 i i i 值,都没有发现可以整除 n n n 的数,那么 n n n 就是素数,我们返回
true
。return true;
在
main
函数中,我们读入 n n n,然后调用isPrime
函数判断 n n n 是否为素数,并输出相应的结果。ll n; cin >> n; if (isPrime(n)) { cout << "Yes" << endl; } else { cout << "No" << endl; }
注意,由于 n n n 的范围可能很大,我们使用了
long long
类型来存储 n n n 和循环变量 i i i,以避免整数溢出。
这个解决方案的时间复杂度是 O ( n ) O(\sqrt{n}) O(n),对于题目给定的范围,是可以通过的。
2 埃拉托斯特尼筛法(Sieve of Eratosthenes)
2.1 埃拉托斯特尼筛法介绍
埃拉托斯特尼筛法是一种简单且历史悠久的算法,用于查找指定数量范围内的所有质数。该算法以其发明者,古希腊数学家埃拉托斯特尼(Eratosthenes)的名字命名。
这个算法基于一个简单的原理:任何一个合数都可以被分解为若干个质数的乘积。因此,我们可以通过逐步排除已知质数的倍数,最终得到所有的质数。
算法的基本步骤如下:
- 创建一个从2到n的整数列表。
- 将2标记为质数,并将2的所有倍数标记为合数。
- 找到下一个未被标记为合数的数,将其标记为质数,并将其所有倍数标记为合数。
- 重复步骤3,直到所有数都被标记。
最终,列表中未被标记为合数的数即为质数。
让我们通过一个例子来演示埃拉托斯特尼筛法的过程。假设我们要找出从0到12的所有质数。
初始状态(假设所有数字均为质数):
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|-------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| T | T | T | T | T | T | T | T | T | T | T | T | T | T |
标记0和1为非质数(因为它们不是质数):
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|-------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| bool | F | F | T | T | T | T | T | T | T | T | T | T | T |
| X | X | X | | | | | | | | | | | |
发现2是质数,加入primes,并筛选2的倍数:
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| bool | F | F | T | T | F | T | F | T | F | T | F | T | F |
| X | | | | | X | | X | | X | | X | | X |
| primes | 2 | | | | | | | | | | | | |
发现3是质数,加入primes,并筛选3的倍数:
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| bool | F | F | T | T | F | T | F | T | F | F | F | T | F |
| X | | | | | | | X | | | X | | | X |
| primes | 2 | 3 | | | | | | | | | | | |
发现5是质数,加入primes,并筛选5的倍数:
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| bool | F | F | T | T | F | T | F | T | F | F | F | T | F |
| X | | | | | | | | | | | X | | |
| primes | 2 | 3 | 5 | | | | | | | | | | |
发现7是质数,加入primes,并筛选7的倍数:
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| bool | F | F | T | T | F | T | F | T | F | F | F | T | F |
| X | | | | | | | | | | | | | |
| primes | 2 | 3 | 5 | 7 | | | | | | | | | |
发现11是质数,加入primes,并筛选11的倍数:
| v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|--------|---|---|---|---|---|---|---|---|---|---|----|----|----|
| bool | F | F | T | T | F | T | F | T | F | F | F | T | F |
| X | | | | | | | | | | | | | |
| primes | 2 | 3 | 5 | 7 | 11| | | | | | | | |
最终,我们得到了从0到12的所有质数:2, 3, 5, 7, 11。
埃拉托斯特尼筛法是一种简单有效的质数筛选算法。它的时间复杂度为 O ( n log log n ) O(n \log \log n) O(nloglogn),空间复杂度为 O ( n ) O(n) O(n)。虽然对于非常大的数字,这个算法可能会变得低效,但对于小至中等规模的数字,它仍然是一个非常实用的质数筛选方法。
2.2 埃拉托斯特尼筛法题目
题目:
输入一个正整数 n n n ( 2 ≤ n ≤ 1 0 6 2 \le n \le 10^6 2≤n≤106),输出 1 1 1 到 n n n 之间(包括 1 1 1 和 n n n)的所有质数。输入格式:
输入只有一行,包含一个正整数 n n n。输出格式:
输出 1 1 1 到 n n n 之间的所有质数,每行输出一个质数。质数按从小到大的顺序输出。示例:
输入:20
输出:
2 3 5 7 11 13 17 19
代码如下:
#include <iostream>
#include <vector>
using namespace std;
const int MAX_N = 1e6 + 1;
vector<bool> isPrime(MAX_N, true);
vector<int> primes;
void sieveOfEratosthenes(int n) {
for (int p = 2; p * p <= n; ++p) {
if (isPrime[p]) {
for (int i = p * p; i <= n; i += p)
isPrime[i] = false;
}
}
for (int p = 2; p <= n; ++p) {
if (isPrime[p])
primes.push_back(p);
}
}
int main() {
int n;
cin >> n;
sieveOfEratosthenes(n);
for (int prime : primes)
cout << prime << endl;
return 0;
}
主要修改:
-
将
isPrime
和primes
数组定义为全局变量,放在所有函数外面。isPrime
数组的大小设为MAX_N
,即题目给定的n的上限1e6+1。primes
数组不需要指定大小,因为它是一个向量,可以动态扩容。
-
将
sieveOfEratosthenes
函数的返回类型改为void
,不再返回素数数组。- 因为
primes
数组已经是全局的,不需要通过函数返回值传递。 - 函数内部直接对全局的
isPrime
和primes
数组进行操作。
- 因为
-
在
main
函数中,调用sieveOfEratosthenes
函数时不需要接收返回值。- 直接遍历全局的
primes
数组,输出其中保存的所有素数。
- 直接遍历全局的
但是要注意,使用全局变量可能会降低代码的可读性和可维护性,需要谨慎使用。
3 线性筛法
3.1线性筛法题目
题目:
输入一个正整数 n n n ( 2 ≤ n ≤ 1 0 7 2 \le n \le 10^7 2≤n≤107),输出 1 1 1 到 n n n 之间(包括 1 1 1 和 n n n)的所有质数。输入格式:
输入只有一行,包含一个正整数 n n n。输出格式:
输出 1 1 1 到 n n n 之间的所有质数,每行输出一个质数。质数按从小到大的顺序输出。示例:
输入:20
输出:
2 3 5 7 11 13 17 19
解决方案:
#include <iostream>
#include <vector>
using namespace std;
const int MAX = 10000000;
vector<int> primes;//存储素数的
vector<bool> is_prime(MAX + 1, true);//标记素数是否为真
void linear_sieve() {
//1.标记0和1为false
is_prime[0] = is_prime[1] = false;
//2.从2开始到Max结束
for (int i = 2; i <= MAX; i++) {
if (is_prime[i]) {
primes.push_back(i);
}
for (int j = 0; j < primes.size() && i * primes[j] <= MAX; j++) {
is_prime[i * primes[j]] = false;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n;
cin >> n;
linear_sieve();
for (int prime : primes) {
if (prime > n)
break;
cout << prime << endl;
}
return 0;
}
解析:
-
我们创建一个布尔型的
is_prime
向量,用来标记每个数是否为质数。初始时,我们假设所有数都是质数,所以is_prime
向量初始化为全true
。向量的大小是MAX + 1
,其中MAX
是题目给定的 n n n 的上限,这里设为 1 0 7 10^7 107。 -
我们还定义了一个整型向量
primes
,用来存储所有找到的质数。 -
接下来,我们实现
linear_sieve
函数,这个函数实现了线性筛法。- 我们首先标记 0 和 1 不是质数。
- 然后,我们从 2 开始遍历到
MAX
。对于每个数i
:- 如果
is_prime[i]
为true
,说明i
是质数,我们将其添加到primes
向量中。 - 然后,我们遍历
primes
向量中的每个质数p
(从小到大),标记i * p
为合数,直到i * p > MAX
或者i % p == 0
。 - 如果
i % p == 0
,说明i
的最小质因数是p
,那么i * p'
对于p' > p
的质数p'
来说,都已经被p'
的倍数标记过了,所以我们可以停止内层的循环。
- 如果
-
在主函数中,我们首先读入 n n n。
-
然后,我们调用
linear_sieve
函数,执行线性筛法。 -
最后,我们遍历
primes
向量,对于每个小于等于 n n n 的质数,我们输出它。如果遇到大于 n n n 的质数,我们就停止遍历。
这个解决方案的时间复杂度是 O ( n ) O(n) O(n),空间复杂度是 O ( n ) O(n) O(n)。它可以在规定的时间和空间限制内解决这个问题。线性筛法避免了埃拉托斯特尼筛法中的重复标记,因此可以在线性时间内找到所有质数。
第五章 区间筛法
5.1 了解区间筛法的基本思路
区间筛法是一种用于在给定区间内筛选素数的算法。它的基本思路是,先使用线性筛法或欧拉筛法预处理出一定范围内的素数,然后利用这些素数在给定区间内进行筛选。
区间筛法的优势在于,它可以在较大的区间内快速筛选素数,而不需要从头开始筛选。这在处理多个区间或者动态查询素数时非常有用。
区间筛法的基本步骤如下:
-
预处理阶段:
- 选择一个适当的上界 R \sqrt{R} R,其中 R R R 是所有查询区间的右端点的最大值。
- 使用线性筛法或欧拉筛法预处理出小于等于 R \sqrt{R} R 的所有素数,存储在一个素数列表 p r i m e s primes primes 中。
-
筛选阶段:
- 对于每个查询区间
[
L
,
R
]
[L, R]
[L,R],执行以下步骤:
- 创建一个长度为 R − L + 1 R-L+1 R−L+1 的布尔数组 i s P r i m e isPrime isPrime,初始时所有元素都为 t r u e true true。
- 对于素数列表
p
r
i
m
e
s
primes
primes 中的每个素数
p
p
p,执行以下操作:
- 找到第一个大于等于 L L L 且能被 p p p 整除的数 s t a r t start start,即 s t a r t = ⌈ L p ⌉ × p start = \lceil \frac{L}{p} \rceil \times p start=⌈pL⌉×p。
- 从 s t a r t start start 开始,将 i s P r i m e isPrime isPrime 数组中所有 p p p 的倍数标记为 f a l s e false false,即 i s P r i m e [ i − L ] = f a l s e isPrime[i-L]=false isPrime[i−L]=false,其中 i = s t a r t , s t a r t + p , s t a r t + 2 p , … , R i=start,start+p,start+2p,\dots,R i=start,start+p,start+2p,…,R。
- 遍历 i s P r i m e isPrime isPrime 数组,对应下标为 t r u e true true 的元素即为区间 [ L , R ] [L, R] [L,R] 内的素数。
- 对于每个查询区间
[
L
,
R
]
[L, R]
[L,R],执行以下步骤:
区间筛法的时间复杂度取决于预处理阶段和筛选阶段的复杂度:
- 预处理阶段的时间复杂度为 O ( R ) O(\sqrt{R}) O(R),其中 R R R 是所有查询区间右端点的最大值。
- 筛选阶段的时间复杂度为 O ( ( R − L + 1 ) log log R ) O((R-L+1)\log\log R) O((R−L+1)loglogR),其中 [ L , R ] [L, R] [L,R] 是要筛选的区间。
区间筛法的优点是可以快速处理多个区间的素数筛选问题,特别是当区间较大且查询次数较多时,它的效率会比直接对每个区间进行筛选要高。
然而,区间筛法的缺点是需要额外的内存空间来存储预处理的素数列表和临时的布尔数组。此外,如果查询区间的右端点 R R R 非常大,预处理阶段的时间复杂度也会较高。
在实际应用中,区间筛法通常与其他优化技术结合使用,如分块、压位等,以进一步提高效率。
总之,区间筛法是一种用于在给定区间内筛选素数的有效算法,特别适用于处理多个区间或动态查询的场景。
5.2 实现区间筛法代码
区间筛法(Segmented Sieve)是一种用来找出所有在给定区间 [ L , R ] [L, R] [L,R] 内的素数的算法。它是对传统的埃拉托斯特尼筛法(Eratosthenes筛法)的改进,特别适用于大区间的情况,可以减少内存的使用,并处理大数范围内的素数筛选问题。
区间筛法的基本思想是将大区间分割成更小的段,然后对每一段使用筛法。为了找出区间 [ L , R ] [L, R] [L,R] 内的所有素数,首先需要用传统的筛法找出所有小于或等于 R \sqrt{R} R 的素数,然后利用这些素数来筛选区间 [ L , R ] [L, R] [L,R] 内的数。
下面是实现区间筛法的C++代码示例:
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1000000; // 最大范围
vector<int> primes; // 存储所有素数
vector<bool> is_prime(MAXN + 1, true); // 标记是否为素数
vector<int> prefix(MAXN + 1, 0); // 前缀和数组
void sieve() {
is_prime[0] = is_prime[1] = false; // 0 和 1 不是素数
for (int i = 2; i <= MAXN; ++i) {
if (is_prime[i]) {
primes.push_back(i);
for (int j = 2 * i; j <= MAXN; j += i) {
is_prime[j] = false;
}
}
}
// 构建前缀和数组
for (int i = 1; i <= MAXN; ++i) {
prefix[i] = prefix[i - 1] + (is_prime[i] ? 1 : 0);
}
}
int countPrimes(int L, int R) {
return prefix[R] - prefix[L - 1];
}
int main() {
sieve(); // 预处理素数和前缀和
int T;
cin >> T;
while (T--) {
int L, R;
cin >> L >> R;
cout << countPrimes(L, R) << endl;
}
return 0;
}