本文只写了较常见的几种素数求法及筛法,实际上方法多种多样,但这些也已经足够用了(学不会其他的)
概念
素数研究是数论中最古老、也是最基本的部分,素数即质数
定义为在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数
性质
1.素数p的约数:1 和 p
2.唯一分解定理:任意大于 1 的自然数,要么本身是素数,要么可以分解为几个素数之积,且这种分解是唯一的
3.素数的个数是无限的
常用的判断素数的方法:
1.试除法
在特判 0 1 2 后,再在区间 [2,n) 之间试图找出一个数能把 n 整除,若找到,则返回 0 ,说明 n 不是素数,若没找到,最终返回 1 ,说明 n 是素数
int f(int n){
if(n<=1)return 0;
if(n==2)return 1;
for(int i=2;i<n;i++)
if(n%i==0)
return 0;
return 1;
}
改进:
对以上代码,如果我们输入 1e9+7 来看看运算时间
这太慢了!在算法竞赛里绝大部分的题都要求在 1s 内出结果
开始优化
我们知道所有自然数的因数都是成对出现的,并且分布在区间 [1,√n] 和 [√n,n] 内,如果因数为 √n 则认为有两个因数 √n ,不过是重复的而已
于是可以对刚才的for做点改动
我们没必要一直试除到 i=n-1 ,因为如果在 [2,√n] 内都找不到能把 n 整除的数,那么 [√n,n) 内也一定找不到能把 n 整除的数
于是乎:
int f(int n){
if(n<=1)return 0;
if(n==2)return 1;
int x=sqrt(n);
for(int i=2;i<=x;i++)
if(n%i==0)
return 0;
return 1;
}
同样输入 1e9+7 试试
ohhh差距异常明显
那再试试 1e18+7 呢(已将数据类型改为long long)
2.取模法
利用数学知识
当 n≥6 时, n 可以表示为 6x+1、6x+2、6x+3、6x+4、6x+5、6x (x≥1) 中的一种
那么,若 n 的表达式为 6x+2、6x+4或6x ,很显然 n 不是素数
故当 n 的表达式为 6x+1或6x+5 时,可能为素数
再应用试除法中改进的方法:
int f(int n) {
if (n == 1)return 0;
if (n == 2 || n == 3)return 1;
if (n % 6 != 5 && n % 6 != 1)return 0;
int x=sqrt(n);
for (int i = 5; i < x; i += 6) {
if (n % i == 0 || n % (i + 2) == 0)
return 0;
}
return 1;
}
我们来直接试试1e18+7
3.埃氏筛
先引入一个问题:
在区间 [ 0,10^6 ] 内进行 10^6 次询问,每次询问输入一个在此区间内的 n ,判断 n 是否为素数
对于该问题,即使我们已经把素数判断写成了函数以多次调用,但调用 10^6 次是否太低效了,我们能否通过一种预处理的方法把 10^6 内的所有素数筛选出来
首先公认的:一个素数的倍数一定不是素数
于是对于一连串自然数: 2 3 4 5 6 7 8 9 10 11 12 13 14 15…
先把2的倍数划去: 2 3 5 7 9 11 13 15…
再把3的倍数划去: 2 3 5 7 11 13…
以此类推,最后留下来的数就是素数
首先准备好一个判断是否素数的数组 vis[N] ,下标为要判断的数,值为 1 代表被筛掉的非素数(数组开在全局时所有值默认为 0 )
上代码
#include<iostream>
using namespace std;
const int N=1e6+7;
int vis[N];
void getprime(){
vis[0]=vis[1]=1;//0 1不是素数
for(int i=2;i<N;i++)
if(!vis[i])//如果i是素数,开筛!
for(int j=i*2;j<N;j+=i)
vis[j]=1;
}
int main(){
getprime();
//打印1000以内的素数
for(int i=0;i<=1000;i++)
if(!vis[i])
cout<<i<<" ";
return 0;
}
这个时间复杂度我算不来。。直接贴 O(nloglogn)
实际上 getprime 还可以优化
开筛时的for先筛了 2 的所有倍数,然后下一次for再去筛 3 的所有倍数
然而筛除 3 的倍数时,我们又从 3 的 2 倍开始筛,其实 3 * 2 ,已经被 2 * 3 时筛过了
再筛 5 的倍数时,从 5 的 2 倍开始筛,但是 5 * 2 会先被 2 * 5 筛去, 5 * 3 会先被 3 * 5 会筛去,所以我们每一次只需要从 i * i 开始筛,因为 n 的 2,3,4…n-1 倍已经被筛过了
另外,判断n是不是质数,我们只判断 [2,√n] 内有没有它的因数
在素数筛中,一样可以这样做,因为一个数的最小质因数一定小于等于√n
所以对于区间 [0,N] ,最大的数是N, 它的最小质因数就一定小于等于√N
所以只需要用 [0,√N] 中的质数就可以筛去 [0,N] 中所有的非素数
于是得到
void getprime() {
vis[0] = vis[1] = 1;//0 1不是素数
int x = sqrt(N);
for (int i = 2; i <= x; i++)
if (!vis[i])//如果i是素数,开筛!
for (int j = i * i; j < N; j += i)
vis[j] = 1;
}
这样时间复杂度接近 O(n)
接下来测试一下筛 [0,10^6 ] 过程所用时间:
如果再增加一个问题,输出从 2 开始的 100 个素数
很简单,只要再开个数组 prime[N] 来存放素数就行了,在 getprime 里进行改动:
void getprime() {
vis[0] = vis[1] = 1;//0 1不是素数
int x = sqrt(N),cnt=0;
for (int i = 2; i <= x; i++)
if (!vis[i]){
prime[cnt++]=i;
for (int j = i * i; j < N; j += i)
vis[j] = 1;
}
}
然后对 prime 数组进行输出:
埃氏筛的缺点,在达到 10^7 以上时时间好像消耗有点多
4.欧拉筛
在埃氏筛中,核心是去筛掉每一个质数的倍数,显然后面会有数被他的多个质因数筛去,过多消耗时间,例如 210 会被 2 3 5 7 各筛一次,于是对针对这种多次筛去同一个数的情况进行优化就得到了欧拉筛,他能把时间复杂度降到真正的 O(n) ,所以又称线性筛
欧拉筛的核心是 保证每个非素数都只会被他的最小质因数筛掉
void getprime() {
int cnt = 0;
vis[0]=vis[1]=1;
for (int i = 2; i < N; i++) {
if (!vis[i]) prime[cnt++] = i;
//遍历已知素数表
for (int j = 0; j < cnt; j++) {
int x=prime[j]*i;
if (x <= N)
vis[x] = 1;//筛掉
else
break;
//当在已知质数表中找到了一个质数能整除i
//说明找到的这个质数prime[j]就是i的最小质因数
if (i % prime[j] == 0) break;
}
}
}
待更新
5.Miller - Rabin 素数检测算法
不更了,M-R太难了
本文程序计时模板
#include <iostream>
#include <time.h>
using namespace std;
int main() {
//clock_t为CPU时钟计时单元数
clock_t start, finish;
//clock()函数返回此时CPU时钟计时单元数
start = clock();
//待测时的代码执行区:
//......
//clock()函数返回此时CPU时钟计时单元数
finish = clock();
//finish与start的差值即为程序运行花费的CPU时钟单元数量,再除每秒CPU有多少个时钟单元,即为程序耗时
cout << "time: " << double(finish - start) / CLOCKS_PER_SEC << "s" << endl;
return 0;
}