1.试除法判定质数
首先回顾一下什么是质数?
- 对所有大于1的自然数,如果这个数的约数只包含1和它本身,则这个数被称为质数或者素数
试除法:对于一个数n,从2枚举到n-1,若有数能够整除n,则说明除了1和n本身,n还有其它约数,则n不是质数;否则,n是质数;
- 优化:由于一个数的约数都是成对出现的。比如12的一组约数是3,4,另一组约数是2,6。则我们只需要枚举较小的那一个约数即可。我们用d|n来表示d整除n,只要满足d|n,则一定有{n/d}|n,比如3∣12,则{12/3} | 12,因为约数总是成对出现的,我们只需要枚举小的那部分数即可,令d ≤ n/d,即,d ≤ sqrt{n},因此对于n,只枚举2到sqrt{n}即可。
- 注意:for循环的结束条件,推荐写成
i <= n / i
。有的人可能会写成i <= sqrt(n)
,这样每次循环都会执行一次sqrt函数,而这个函数是有一定时间复杂度的。而有的人可能会写成i * i <= n
,这样当i很大的时候(比如i比较接近int的最大值时),i * i
可能会溢出,从而导致结果错误。
Acwing 866.试除法判定质数
具体实现代码(详解版):
#include <iostream>
using namespace std;
//试除法:用于判断传入的整数 x 是否为素数
bool is_prime(int x){
// 如果 x 小于 2,则不是素数,返回 false
if(x < 2) return false;
// 从 2 开始迭代,直到 i * i <= x (相当于 i <= sqrt(x)),检查是否有因数
for(int i = 2; i <= x / i; i++){
// 如果 x 能被 i 整除,说明 x 不是素数,返回 false
if(x % i == 0)
return false;
}
// 如果没有找到任何因数,返回 true,表示 x 是素数
return true;
}
int main(){
int n, x;
cin >> n;
while(n --){
cin >> x;
// 如果 x 是素数,输出 "Yes",否则输出 "No"
if(is_prime(x)) puts("Yes");
else puts("No");
}
return 0;
}
2.质因数分解:试除法
对于一个整数 N 总能写成如下形式:
N
=
P
1
α
1
×
P
2
α
2
×
P
3
α
3
⋯
×
P
n
α
n
N=P_{1} ^{α_1} \times P_{2} ^{α_2} \times P_{3} ^{α_3} \dots \times P_{n} ^{α_n}
N=P1α1×P2α2×P3α3⋯×Pnαn
其中Pi都是质数,αi为大于0的正整数,即一个整数可以表示为多个不同质数的次方的乘积
- 对于一个数求质因数的过程:从2到n,枚举所有数,依次判断是否能够整除n即可。朴素法,时间复杂度O(n))。;
- 优化:n中只包含一个大于sqrt{n}的质因子,很好证明,如果中包含两个大于sqrt{n}的质因子,那么乘起来就大于n了。因此,在枚举的时候可以先把2到sqrt{n}的质因子枚举出来,如果最后处理完n > 1,那么这个数就是那个大于\sqrt{n}的质因子,单独处理一下就可以。时间复杂度降为O(sqrt(n))
求质因数分解,为什么枚举所有数,而不是枚举所有质数,万一枚举到合数怎么办?解释:枚举数时,对于每个能整除 n 的数 i,先把这个数除干净了(就是把这个质数的次方剔除了,表现在上式中就是逐步去除Pi^αi),再继续枚举后面的数,这样能保证,后续再遇到能整除的数,一定是质数而不是合数。
例如:求180的质因数分解
- i = 2 n = 180 / 2 = 90 / 2 = 45
- i = 3 n = 45 / 3 = 15 / 3 = 5
- i = 4 当i是合数时,i 一定不能整除 n 。如果 4 能整除 n 那么 2 一定还能整除 n,就是在 i = 2的时候没有除干净,而我们对于每个除数都是除干净的,因此产生矛盾。
- i = 5 n = 5 / 5 = 1
Acwing 867.分解质因数
具体实现代码(详解版):
#include <iostream>
using namespace std;
// 使用试除法分解质因数
void divide(int x){
// 从 2 开始迭代,直到 i * i <= x (相当于 i <= sqrt(x)),试图找到因数
for(int i = 2 ; i <= x / i ; i ++){
// 如果 x 能被 i 整除,则 i 是 x 的一个质因数
if(x % i == 0){
int s = 0; // 计数 i 的出现次数
// 用 while 循环找出 i 作为因子的次数,直到 x 不能被 i 整除
while(x % i == 0) x /= i, s ++;
cout << i << ' ' << s << endl;
}
}
// 如果 x 最后还大于 1,说明剩下的 x 是一个大于 sqrt(x) 的质数
if(x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
int main(){
int n, x;
cin >> n;
while(n --){
cin >> x;
divide(x);
}
return 0;
}
3.筛质数–朴素法
将2到n全部数放在一个集合中,遍历2到n,每次删除当前遍历的数在集合中的倍数。最后集合中剩下的数就是质数。
解释:如果一个数p没有被删掉,那么说明在2到p-1之间的所有数,p都不是其倍数,即2到p-1之间,不存在p的约数。故p一定是质数。
时间复杂度:
n 2 + n 3 + ⋯ + n n = n ln n < n log 2 n \frac{n}{2} + \frac{n}{3} + \dots +\frac{n}{n} = n\ln_{}{n} < n\log_{2}{n} 2n+3n+⋯+nn=nlnn<nlog2n
故,朴素思路筛选质数的时间复杂度大约为O(nlogn)
Acwing 868.筛质数
具体实现代码(详解版):
#include <iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt; // primes 数组存储所有找到的素数,cnt 计数素数的个数
bool st[N]; // st 数组标记数字是否被筛掉,false 表示未筛掉,true 表示已筛掉
// 朴素筛法,用于筛选出小于等于 n 的所有素数
void get_prime(int n) {
// 从 2 开始遍历到 n,逐个检查数字是否是素数
for (int i = 2; i <= n; i++) {
// 如果 st[i] 为 false,说明 i 没有被筛掉,因此 i 是素数
if (!st[i]) primes[cnt++] = i;
// 将所有 i 的倍数标记为 true,表示这些数不是素数
for (int j = i + i; j <= n; j += i) {
st[j] = true;
}
}
}
int main() {
int n;
cin >> n;
get_prime(n);
cout << cnt << endl;
return 0;
}
4.筛质数–埃氏筛法
在上面朴素筛法的基础上,
其实不需要把全部数的倍数删掉,而只需要删除质数的倍数即可。
对于一个数p,判断其是否是质数,其实不需要把2到p-1全部数的倍数删一遍,只要删掉2到p-1之间的质数的倍数即可。因为,若p不是个质数,则其在2到p-1之间,一定有质因数,只需要删除其质因数的倍数,则p就能够被删掉。埃氏筛法筛选质数的时间复杂度大约为O{nlog(logn)}
具体实现代码(详解版):
#include <iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt; // primes 数组存储所有素数,cnt 记录素数的个数
bool st[N]; // st 数组用于标记每个数是否被筛掉,false 表示未筛掉
// 埃拉托斯特尼筛法,找出小于等于 n 的所有素数
void get_prime(int n) {
// 从 2 开始遍历,检查每个数是否是素数
for (int i = 2; i <= n; i++) {
// 如果 i 没有被筛掉,则 i 是一个素数
if (!st[i]) {
primes[cnt++] = i; // 将素数存储到 primes 数组中,并增加素数个数计数
// 将 i 的所有倍数标记为非素数,从 i * 2 开始,每次增加 i
for (int j = i + i; j <= n; j += i)
st[j] = true; // 标记 i 的倍数为 true,表示它们不是素数
}
}
}
int main() {
int n;
cin >> n; // 输入一个整数 n,表示筛选范围为 2 到 n
get_prime(n); // 调用 get_prime 函数进行素数筛选
cout << cnt << endl; // 输出素数的个数
return 0;
}
5.筛质数–线性筛法
大体思路和埃氏筛法一样,将合数用他的某个因数筛掉,其性能要优于埃氏筛法(在 1 0 6 10^{6} 106下两个算法差不多,在10^7下线性筛法大概快一倍)核心思路是:对于某一个合数n,其只会被自己的最小质因子给筛掉,从而避免了重复标记
设置一个primes数组,存储质数(以下叙述用pj来表示primes[j]),从2到n进行循环遍历,用数组st[]标记是否为质数。每次循环都对当前质数数组进行遍历,用其最小质因子筛除合数
- 当
i % pj == 0
时:pj 一定是 i 的最小质因子,因为我们是从小到大枚举质数的,首先遇到的满足i % p j == 0
的,pj 一定是 i 的最小质因子,并且pj 一定是pj * i
的最小质因子。比如,15 = 3 *5,15的最小质因子是3,则15的倍数中最小的数,其最小质因子同样是3的,15乘以最小质因子3,即45; - 当
i % pj != 0
时:pj 一定不是 i 的质因子,并且由于是从小到大枚举质数的,那么 pj 一定小于 i 的全部质因子。那么 pj 就一定是pj * i
的最小质因子
具体实现代码(详解版):
#include <iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt; // primes 数组存储所有找到的素数,cnt 记录素数的个数
bool st[N]; // st 数组标记数字是否被筛掉,false 表示未筛掉
// 线性筛法,筛选出小于等于 n 的所有素数
void get_prime(int n) {
// 遍历 2 到 n,逐个判断每个数是否为素数
for (int i = 2; i <= n; i++) {
// 如果 st[i] 为 false,则 i 是素数,将其存入 primes 数组
if (!st[i]) primes[cnt++] = i;
// 遍历所有已找到的素数 primes[j]
for (int j = 0; primes[j] <= n / i; j++) {
// 将 i 与当前素数 primes[j] 的乘积标记为合数
st[primes[j] * i] = true;
// 如果 i 是 primes[j] 的倍数,停止筛选,因为后面的乘积已经包含该素数
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n;
cin >> n;
get_prime(n);
cout << cnt << endl;
return 0;
}
对比总结:
筛法名称 | 时间复杂度 | 空间复杂度 | 算法思想 | 优缺点 |
---|---|---|---|---|
朴素筛法 | O(n√n) | O(1) | 对于每个数 i ,依次检查它是否是素数,若是素数,将其倍数标记为合数。 | 优点:实现简单,代码直观。缺点:时间复杂度较高,筛选效率低。 |
埃拉托斯特尼筛法 | O(n log log n) | O(n) | 从 2 开始,对于每个素数 i ,将它的所有倍数标记为合数。 | 优点:效率比朴素筛法高。缺点:每个合数可能被多次标记(即被多个素数筛掉)。 |
线性筛法 | O(n) | O(n) | 对每个数 i ,只用它的最小质因子来筛选它的倍数,保证每个合数只被标记一次。 | 优点:筛选效率最高,时间复杂度接近线性。缺点:实现稍复杂,理解难度较高。 |
以上基本可以解决所有素数的问题,多写几遍板子。