定义
若一个正整数无法被除了 1 和它自身之外的任何自然数整除,则称该数为质数(或素数),否则该正整数为合数。
在整个自然数集合中,质数的数量不多,分布比较稀疏,对于一个足够大的整数 N,不超过 N 的质数大约有 N/lnN 个,即每 lnN 个数中大约有 1 个质数。
质数的判定
我们最常用的方法是“试除法”
简单来说就是,扫描 2~ 之间的所有整数,依次检查它们能否整除 N,若都不能整除,则 N 是质数,否则 N 是合数。试除法的时间复杂度为 O()。当然,我们需要特判 0 和1 这两个整数,它们既不是质数,也不是合数。
代码示例
bool is_prime(int n){
if(n<2) return false;
for(int i=2;i<=sqrt(n);i++){
if(n%i==0) return false;
}
return true;
}
补充:Miller-Robbin算法
算法是基于费马小定理(format),二次探测定理(x*x % p == 1 ,若P为素数,则x的解只能是x = 1或者x = p - 1)加上迭代乘法判断的Miller算法。适用于测试单个素数,出错概率比计算机本身出错的概率还要低。
这里借用别人的总结(就是我自己懒)
费马小定理:
如果p是一个素数,且0<a<p,则a^(p-1)%p=1。
利用费马小定理,对于给定的整数n,可以设计素数判定算法,通过 计算d=a^(n-1)%n来判断n的素性,当d!=1时,n肯定不是素数,当d=1时,n 很可能是素数。
二次探测定理:
如果p是一个素数,且0<x<p,则方程x^2%p=1的解为: x=1或x=p-1。
利用二次探测定理,可以再利用费马小定理计算a^(n-1)%n的过程中增加对整数n的二次探测,一旦发现违背二次探测条件,即得出n不是素数的结论。
如果n是素数,则(n-1)必是偶数,因此可令(n-1)=m*(2^q),其中m是正奇数( 若n是偶数,则上面的m*(2^q)一定可以分解成一个正奇数乘以2的k次方的形式 ),q是非负整数,考察下面的测试:
序列:a^m%n; a^(2m)%n; a^(4m)%n; …… ;a^(m*2^q)%n
把上述测试序列叫做Miller测试,关于Miller测试,有下面的定理:
定理:若n是素数,a是小于n的正整数,则n对以a为基的Miller测试,结果为真。
Miller测试进行k次,将合数当成素数处理的错误概率最多不会超过4^(-k)。
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<time.h>
using namespace std;
int n;
int witness(int a, int n){//随机生成的a,来检测n的素性
int ans=1;
int t=n-1;//这里需要注意,你如果没有改变乘方的次数的话,最后的判断就是(ans == a) ? 0 : 1;
// 并且还要另外开辟空间来存储开始的a,比较麻烦,所以就这样了;
int x;
while(t){
if(t&1) ans = (long long int)ans * a % n;
x=a;//从这里开始就是迭代乘法,验证二次验证定理
a=(long long)a*a%n;//这里就相当于 x*x % m = 1
if(a==1&&x!=1&&x!=(n - 1)) return 1;// 这里需要注意,返回一的话就说明,追踪过程中,出现了不是素数的依据.
t>>=1;
}
return (ans==1)?0:1;
}
int MillerRobin(int n, int s){ // 一般s取50就可以避免所有的偶然性了.
if(n==2) return 1;
if(n<2||!(n&1)) return 0;
int a;
for(int i=0;i<s;i++){
a=(long long)rand()*(n-2)/RAND_MAX + 1; //这样生成的随机数就是真正的随机数了
if(witness(a, n)) return 0;
}
return 1;
}
int main(){
cin>>n;
if(MillerRobin(n,50)) cout<<n<<" is a prime!"<<endl;
else cout<<n<<" is not a prime!"<<endl;
return 0;
}
质数的筛选
给定一个整数 N,求出 1~N 之间的所有质数,称为质数的筛选问题。
Eratosthenes 筛法
这个筛法基于这样的想法:任意整数 x 的倍数 2x,3x,... 都不是质数。
我们可以从 2 开始,由小到大扫描每个数 x,把它的倍数标记为合数。当扫描到一个数时,若它尚未被标记,则它不能被 2~x-1 之间的任何数整除,该数就是质数。
另外,我们可以发现,小于 x^2 的 x 的倍数在扫描更小的数时就已经被标记过了。因此,我们可以对 Eratosthenes 筛法进行优化,对于每个数 x,我们只需要从 x^2 开始,到 N/x * x 之间所有的数标记为合数即可。
代码示例
void prime(int n){
memset(vis,0,sizeof(vis));
for(int i=2;i<=n;i++){
if(vis[i]) continue;
cout<<i<<endl;
for(int j=i;j<=n/i;j++) v[i*j]=1;
}
}
这个筛法效率已经非常接近于线性O(NloglogN),是比赛中比较常用的质数筛法。
线性筛法
Eratosthenes 筛法即使在优化后(从 x^2 开始),仍然会重复标记合数。
线性筛法通过“从大到小累积质因子”的方式标记每个合数,设数组 v 记录每个数的最小质因子,我们按照以下步骤维护 v。
- 依次考虑 2~N 之间的每一个数 i
- 若 v[i]=i,说明 i 是质数,把它保存下来
- 扫描不大于 v[i] 的每个质数 p,令 v[i*p]=p。也就是在 i 的基础上累积一个质因子 p。因为 p≤ v[i],所以 p 就是合数 i*p 的最小质因子
每个合数 i*p 只会被它的最小质因子 p 筛一次,时间复杂度为 O(N)。
void primes(int n){
memset(v,0,sizeof(v));
m=0;
for(int i=2;i<=n;i++){
if(v[i]==0){
v[i]=i;
prime[++m]=i;
}
for(int j=1;j<=m;j++){
if(prime[j]>v[i]||prime[j]>n/i) break;
v[i*prime[j]]=prime[j];
}
}
for(int i=1;i<=m;i++) cout<<prime[i]<<endl;
}
质因数分解
算术基本定理
任何一个大于 1 的正整数都能唯一分解为有限个质数的乘积
其中 ci 都是正整数,pi 都是质数,且满足 p1<p2<...<pm
试除法
结合质数判定的“试除法”和质数筛选的“Eratosthenes 筛法”,我们可以扫描 2~ 的每个数 d,若 d 能整除 N,则从 N 中除掉所有的因子 d,同时累计除去的 d 的个数。
因为一个合数的因子一定在扫描到这个合数之前就从 N 中被除掉了,所以在上述过程中能整除 N 的一定是质数。最终就得到了质因数分解的结果,易知时间复杂度为 O()。
特别地,若 N 没有被任何 2~ 的数整除,则 N 是质数,无需分解。
void divide(int n){
m=0;
for(int i=2;i<=sqrt(n);i++){
if(n%i==0){
p[++m]=i;
c[m]=0;
while(n%i==0){
n/=i;
c[m]++;
}
}
}
if(n>1){
p[++m]=n;
c[m]=1;
}
for(int i=1;i<=m;i++) cout<<p[i]<<"^"<<c[i]<<endl;
}
素数的相关定理
威尔逊定理
若 p 为素数,则(p-1)!-1(mod p) (其中n! 表示n阶乘)
同时,威尔逊定理的逆定理也成立。
既然如此,则(p-1)! + 1 一定是 p 的倍数,所以再利用 sin 函数的特点,就可以构造出一个素数分布的函数曲线 f(n):f(n)=sin(Π*((n-1)!+1)/n)。这个函数值为 0 的点都是素数所在的点。
费马定理
若 p 为素数,a为正整数,且 a 和 p 互质,则:
其实这是一种特殊形式,一般情况下,若 p 为素数,则:,这就是费马小定理