前言
从暴力的直接循环开始,一步步对算法进行优化,一步步简化时间复杂度,并尽可能讨论各种筛法的优劣,以及其中每一步的意义。
一、暴力循环
1.优点
思路直接,容易被像我们这样的新手第一时间想到,一个质数,除了1和它本身以外不再有其他因数,那么想知道一个数是不是质数,干脆直接去看看它有几个因子
2.小技巧
对于大于4的素数,总是可以用6x+1或6x-1来表示
讨论:任意一个自然数都可以用
6x、6x+1、6x+2、6x+3、6x+4、6x+5中的某个来表示
而6x、6x+2、6x+3、6x+4都存在直观可见的因子,故不会为素数。
3.关键代码
仅仅提供思路,理解后能灵活运用即可
代码如下:
#include<iostream>
#include<math.h>
using namespace std;
int main(){
int n,m=0;
cin>>n;
for(int j=2;j<n;j++){/*
也可将n改为pow(n,0.5),取平方根,能够进一步简化运算过程
不过这样的话每次都要调用一下函数,比较慢;
也有的会写为i*i<=n,不过存在溢出风险,使其变为负数,影响结果判断;
最好写为i<=n/i,不存在溢出风险*/
if(n%j==0){
m=1;break;
}
}
(m==1)?cout<<"NO":cout<<"YES";
}
4.缺点
显而易见,当n取值过大时,一次次的判断将极大地影响工作效率,特别是在解决一些有时间要求的算法题时,这样的思路很容易引发超时。
二、埃氏筛
1.改进
任一合数仅能被唯一分解成有限个素数的乘积,意味通过对一个已知质数的处理可以选择出其他与之相关的合数,比如2为质数,通过对2的倍数处理,可以选出4,6,8等以2为因子的合数。
时间复杂度为O(n* log log n)
2.关键代码
代码如下:
#include<iostream>
#include<math.h>
using namespace std;
int a[100005];
int vis[100005];
void f(int n){
int cnt=0;
for(int i=2;i<=n;i++){
if(!vis[i]){//进行判断,若为已被标记的合数则会直接跳过
a[cnt++]=i;
for(int j=2;j*a[cnt-1]<=n;j++){
vis[j*a[cnt-1]]=1;//对范围内该质数的倍数进行标记
}
}
}
}
int main(){
int n;
cin>>n;
f(n);
for(int i=0;a[i]!=0;i++){
cout<<a[i]<<" ";
}
}
3.缺点
以12为例,在整个标记过程中,在处理2的倍数时,被标记过了一次,然后在处理3的倍数时,又被重新标记了一次,若数据较小,则影响不大,但在提交答案时,系统会测试一些处于边界条件下的数据,一次次的重复标记同样会影响效率,可能会导致超时。
三.欧拉筛法
1.改进
埃氏筛存在重复标记的问题,那么想解决这个问题,就需要对每次对合数的标记进行一个规范,从而达到一个合数将有且只有一种标记方式,我们想到的方法是将一个合数化为其最小质因数与另一个数之积。
时间复杂度为O(n)
2.关键代码
#include<iostream>
using namespace std;
int a[100005];
int cnt=0;
int p[100005];
void f(int n){
for(int i=2;i<=n;i++){
if(!p[i])a[cnt++]=i;
for(int j=0;j<cnt&&i*a[j]<=n;j++){
p[i*a[j]]=1;
/*对合数进行标记,并且是依据最小质因数来进
行标记,可以使用反证法来说明:
若此时的质数a[j]不是最小质因数,令m=i*a[j],那么必定存在
另一个数为m的最小质因数,令该数为x,那么存在m=x*y,并且
存在x<a[j],y>i的关系,然而在整个过程中,i一直是从小到
大依次使用的,不可能突然出现一个比i大的y*/
if(i%a[j]==0)break;
/*最关键的一步,用来避免重复标记,可以通过假设法来理解:
此时i恰为a[j]的倍数,不妨令i=k*a[j],然后循环继续进行,
在下一步,令m=i*a[j+1],m同样等于k*a[j]*a[j+1],此时再
看,对于m来说,它的最小质因数并非a[j+1],仍是a[j],意味
着发生了重复标记,违背了我们一开始的想法*/
}
}
}
int main(){
int n;
cin>>n;
f(n);
for(int i=0;a[i]!=0;i++){
cout<<a[i]<<" ";
}
}
4.质数距离
1.分析
1.左右端点的范围很大,但两点间距却不大,所以我们不必开一个那么大的数组,只需要开一个大小为1000 005的数组就够了,过程中把结果处理一下就可以了
2.判断一个数是否为素数,我们知道任何一个自然数都可以唯一分解为质因数之积,那么若为约数,必定存在一个处于2到 根号n的质因数,即便原来的数据很大,我们也只需要看它在这一范围内是否存在质因数,那么我们也就缩减了数据
3。左端点小于2时化为2,不然后面不好确定筛选质因数大于左端点的第一个位置
4.因为存在乘法,过程中可能会出现爆int的情况,所以直接使用long long 吧
.此处为多组输入!!!
2.代码
#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
ll l,r;
ll prim[1000005];
ll prim2[1000005];
ll a[1000005];
ll cnt;
ll ma,mi;
void gp(int n){
for(int i=2;i<=n;i++){
if(!a[i])prim[cnt++]=i;
for(int j=0;j<cnt&&prim[j]<=n/i;j++){
a[i*prim[j]]=1;
if(i%prim[j]==0)break;
}
}
}
int main(){
gp(1000001);
while(cin>>l>>r){
if(l<=1)l=2;
memset(a,0,sizeof(a));
memset(prim2,0,sizeof(prim2));
for(int i=0;;i++){
if(prim[i]*prim[i]>r)break;
ll k;
k=l/prim[i]+(l%prim[i]>0);
if(k==1)k=2;
for(int j=k;j*prim[i]<=r;j++)
if(j*prim[i]>=l)a[j*prim[i]-l]=1;
}
cnt=0;
for(int i=0;i<=r-l;i++){
if(a[i]==0)prim2[cnt++]=l+i;
}
mi=ma=1;
for(int i=1;i<cnt;i++){
int k=prim2[i]-prim2[i-1];
if(k<prim2[mi]-prim2[mi-1])mi=i;
if(k>prim2[ma]-prim2[ma-1])ma=i;
}
if(cnt<2)cout<<"There are no adjacent primes."<<endl;
else cout<<prim2[mi-1]<<","<<prim2[mi]<<" are closest, "<<prim2[ma-1]<<","<<prim2[ma]<<" are most distant."<<endl;
}
}