质数的定义
质数又称素数,是指在大于 1 1 1 的自然数中,除了 1 1 1 和它本身以外不再有其他因数的自然数。
质数的判定
暴力筛选法
假设我们要判定
n
n
n 是否为质数,
n
n
n 暴力做法是从
2
2
2 枚举到
n
−
1
n-1
n−1,看其中是否存在一个自然数可以整除
n
n
n。有一个要注意的点,要特判
n
=
2
n=2
n=2 的情况,直接就返回是即可。时间复杂度是:
O
(
n
)
\mathcal{O}(n)
O(n)。
代码:
bool isp(int n){
if(n<=2){
return n>=2;
}
for(int i=2;i<n;i++){
if(n%i==0){
return 0;
}
}
return 1;
}
暴力筛选法优化
这样做是十分稳妥了,但是真的有必要每个数都去判断吗?
很容易发现这样一个事实:如果
x
x
x 是
a
a
a 的约数,那么
a
x
\frac{a}{x}
xa 也是
a
a
a 的约数。
这个结论告诉我们,对于每一对
(
x
,
a
x
)
(x,\frac{a}{x})
(x,xa),只需要检验其中的一个就好了。为了方便起见,我们之考察每一对里面小的那个数。不难发现,所有这些较小数就是
[
1
,
a
]
[1,\sqrt{a}]
[1,a] 这个区间里的数。时间复杂度为:
O
(
n
)
\mathcal{O}(\sqrt{n})
O(n)。
代码:
bool isp(int n){
if(n<=2){
return n>=2;
}
for(int i=2;i*i<=n;i++){
if(n%i==0){
return 0;
}
}
return 1;
}
Fermat 素性测试
Fermat 素性检验是最简单的概率性素性检验。
我们可以根据费马小定理得出一种检验素数的思路:
基本思想是不断地选取在
[
2
,
n
−
1
]
[2,n-1]
[2,n−1] 中的基
a
a
a,并检验是否每次都有
a
n
−
1
≡
1
(
m
o
d
n
)
a^{n-1} \equiv 1 \pmod n
an−1≡1(modn)。
代码:
#include<bits/stdc++.h>
using namespace std;
int qp(int a,int b,int c){
int n=1;
while(b>0){
if(b&1){
n=(n*a)%c;
b--;
}
a=(a*a)%c;
b>>=1;
}
return n%c;
}
bool isp(int n){
if(n<=2) return n==2;
// test为测试次数,建议设为不小于 8
// 的整数以保证正确率,但也不宜过大,否则会影响效率
int test=10;
for(int i=1;i<=test;i++) {
int a=rand()%(n-2)+2;
if(qp(a,n-1,n)!=1) return 0;
}
return 1;
}
Miller-Rabin 质数检测
根据上面的 Fermat 素性测试,我们可以加上一个二次探测定理即可实现更准确的判断。如果 p p p 是奇素数, x 2 ≡ 1 ( m o d p ) x^2 \equiv 1 \pmod p x2≡1(modp) 的解为 x ≡ 1 ( m o d p ) x \equiv 1 \pmod p x≡1(modp) 或者 x ≡ p − 1 ( m o d p ) x \equiv p - 1 \pmod p x≡p−1(modp)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int qp(int a,int b,int c){
int n=1;
while(b>0){
if(b&1){
n=(n*a)%c;
b--;
}
a=(a*a)%c;
b>>=1;
}
return n%c;
}
bool millerRabin(int n){
if(n<3||n%2==0) return n==2;
if(n%3==0) return n==3;
int u=n-1,t=0;
while(u%2==0) u/=2,t++;
//test_time 为测试次数,建议设为不小于 8
//的整数以保证正确率,但也不宜过大,否则会影响效率
int test_time=10;
for(int i=0;i<test_time;i++){
//0,1,n-1可以直接通过测试, a 取值范围 [2, n-2]
int a=rand()%(n-3)+2,v=qp(a,u,n);
if(v==1) continue;
int s;
for(s=0;s<t;s++){
if (v==n-1) break;//得到平凡平方根 n-1,通过此轮测试
v=v*v%n;
}
//如果找到了非平凡平方根,则会由于无法提前 break; 而运行到 s == t
//如果 Fermat 素性测试无法通过,则一直运行到 s == t 前 v 都不会等于 -1
if(s==t) return 0;
}
return 1;
}
main(){
return 0;
}
埃氏筛
如果我们要筛选出从
1
1
1 到
n
n
n 的质数怎么办呢?我们就要用到埃氏筛了。先从
2
2
2 开始枚举质数,把从
1
1
1 到
n
n
n 范围内能被当前数整除的自然数,都标记为合数,然后找到下一个质数重复操作。时间复杂度为:
O
(
n
log
log
n
)
\mathcal{O}(n\log{\log{n}})
O(nloglogn)。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int k,p[500005];
bool isp[500005];
void work(int n){
isp[0]=isp[1]=0;
for(int i=2;i<=n;i++){
isp[i]=1;
}
for(int i=2;i<=n;i++) {
if(isp[i]==1){
p[++k]=i;
if(i*i>n) continue;
for(int j=i*i;j<=n;j+=i)
// 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i 的倍数开始,提高了运行速度
isp[j]=0; // 是 i 的倍数的均不是素数
}
}
}
main(){
return 0;
}
线性筛
埃氏筛法仍有优化空间,它会将一个合数重复多次标记。有没有什么办法省掉无意义的步骤呢?答案是肯定的。如果能让每个合数都只被标记一次,那么时间复杂度可以降到 O ( n ) \mathcal{O}(n) O(n)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int k,p[100005];
bool vis[100005];
void work(int n){
for(int i=2;i<=n;i++){
if(vis[i]==0) p[++k]=i;
for(int j=1;j<=k&&p[j]*i<=n;j++){
vis[p[j]*i]=1;
if(i%p[j]==0){
// 换言之,i 之前被 pri_j 筛过了
// 由于 pri 里面质数是从小到大的,所以 i 乘上其他的质数的结果一定会被
// pri_j 的倍数筛掉,就不需要在这里先筛一次,所以这里直接 break 掉就好了
break;
}
}
}
}
main(){
return 0;
}
参考资料
https://oi-wiki.org/math/number-theory/sieve/
https://oi-wiki.org/math/number-theory/prime/
https://baike.baidu.com/item/质数/263515#3
https://blog.youkuaiyun.com/qq_43695957/article/details/116062333