素数筛法
作者:互联网
这篇博客是按照我学习素数的顺序写的,算法也是一步步推进,一步步优化的,想找最高效的算法可以直接翻到欧拉筛。这是我的第一篇博客,欢迎各路大神指正错误以及提供建议。
素数定义
质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。
0和1既不是质数,也不是合数。
2是最小的素数,也是唯一一个偶素数。
在比赛中,经常碰到素数相关的题目,下面介绍几种常见的素数判断方法。
基本方法
从2到a-1进行枚举,如果存在i使a能被i整除(a%i==0成立),则a为合数。如果不存在这样的i,则a为质数。
以下代码功能为:输入一个数,判断是否为质数。
#include <bits/stdc++.h> //“万能头文件”
using namespace std;
typedef long long ll;//为编码方便,进行typedef
bool isprime(ll a)
{
ll i;
for(i=2;i<a;i++)
if(a%i==0) break;
if(a==i) return true;//如果在2到a-1都没有找到能整除a的值,则a为质数
else return false;//循环中找到了能整除a的值,break过,导致a不等于i
}
int main()
{ //主函数
ll a;
cin>>a;
if(isprime(a)) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
return 0;
}
时间复杂度:对每一个数,isprime函数最多运算a-2次,时间复杂度接近\(O(a)\)。
初步优化
内容
实际上,我们只需要枚举\(\sqrt a\)次即可完成判断,可将时间复杂度优化到\(O(\sqrt a)\)。
原理
利用反证法,假设一个数a,在2到\(\sqrt a\)之间没有找到a的因数,却在\(\sqrt a+1\)到\(a-1\)之间找到一个a的因数i,则必存在a的另一个因数\(j=\frac{a}{i}\)。因为\(i>\sqrt a\),可得\(\frac{1}{i}<\frac{\sqrt a}{a}\),可得\(j=\frac{a}{i}<\sqrt a\),与假设矛盾。
bool isprime(ll a)
{
ll i;
if(a<=1) return false;//防止某些题目出0、1一类的数据
for(i=2;i*i<=a;i++)
if(a%i==0) return false;
return true;
}
素数筛法
现在我们要输出所有n以内的素数,请在1s内完成。(n<=106)
如果我们仍然按照刚才的方法来解决这个问题,由于每一次询问的时间复杂度为\(O(\sqrt n)\),那m次询问的总复杂度会达到O(n1.5),运算量的数量级为109,那么等待我们的就会是——
Time Limit Exceeded.(时间超限)
所以我们需要更高效的做法,于是就有了素数筛选法。
埃氏筛
原理
当一个数i是素数时,i的所有倍数必然是合数,就可以从素数集中划去。如果i已经判断为合数了那就不必划掉i的倍数了,因为i的倍数已经被i的素因子划掉了。
做法
我们可以建立一个2到n的集合,把最小数2保留,2的倍数划掉。然后寻找最小的下一个数3,由于它没有被划掉,它也就无法被比他小的数整除,因此它是素数。此时,再划掉3的所有倍数。再找下一个数5(4被2划掉了,因此4是合数,无需考虑),划掉5的所有倍数。这样反复操作,就能枚举n以内的素数。
模拟
我们可以模拟一下寻找30以内的素数(加粗代表新划去的数):
- 建立集合
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - 筛除2的倍数:4 6 8 10 12 14 16 18 20 22 24 26 28 30
2 3 5 7 9 11 13 15 17 19 21 23 25 27 29 - 筛除3的倍数:6 9 12 15 18 21 24 27 30
2 3 5 7 11 13 17 19 23 25 29 - 4为合数跳过4(之后合数省略)
- 筛除5的倍数:10 15 20 25 30
2 3 5 7 11 13 17 19 23 29 - 筛除7的倍数:14 21 28
2 3 5 7 11 13 17 19 23 29 - 筛除11的倍数:22
2 3 5 7 11 13 17 19 23 29 - 筛除13的倍数:26
2 3 5 7 11 13 17 19 23 29 - 筛除17、19、23、29的倍数:均不含小于30的倍数
... ... - 筛除到30,算法结束,30以内素数为:
2 3 5 7 11 13 17 19 23 29
通过模拟我们发现从7开始没有筛除掉任何新的素数,因此我们可以进行优化。
事实上我们只需要枚举到\(\sqrt n\)。
原因:我们每次新划去的数只会是i的倍数中不含小于i的素因子的数,所以这些数必然包含大于等于的i素因子,所以这些数一定都是大于等于i2的数。
代码实现
建立一个比较大的bool数组isprime[],其大小取决于数据规模,比如n+5。为方便我们将素数记为false(因为bool全局变量初始值为false)从2开始,从小到大筛选每一个数i,如果i是素数,就将所有的i的倍数置为true,即筛掉所有该素数的倍数,否则不做任何处理,直接筛选下一个数。
#include <iostream>
#include <cstdio>
const int N=1e7+5;
using namespace std;
bool isprime[N]={true,true};//将0、1筛去
int p[N];
int main()
{
int n;
cin>>n;
//筛法开始
for(int i=2;i*i<=n;i++)
{
if(!isprime[i])
for(int j=i+i;j<=n;j+=i)
isprime[j]=true;//筛去j的倍数
}
int k=1;
p[0]=2;
for(int i=3;i<=n;i+=2)//因为除2外无偶素数,可以只遍历奇数
if(!isprime[i])
p[k++]=i;//将素数按照顺序存入数组中
//筛法结束
for(int i=0;i<k;i++)
printf("%d ",p[i]);
return 0;
}
这样的时间复杂度是O(nloglogn)。其实我不知道是怎么算出来的
相比O(n1.5)的暴力做法快了很多,但我们还有更高效的做法。
欧拉筛
优化原理
由刚刚埃氏筛的模拟我们不难发现,许多合数被筛选了多次,而我们在埃氏筛法的基础上进行优化,让每个合数只被它的最小质因子筛选一次,避免重复筛选,可以得到接近O(n)的时间复杂度。
先上代码:
#include <iostream>
#include <cstdio>
const int N=1e7+5;
using namespace std;
bool isprime[N]={true,true};
int p[N];
int main()
{
int n;
cin>>n;
//筛法开始
int k=0;
for(int i=2;i<=n;i++)
{
if(!isprime[i]) p[k++]=i;//判到哪打到哪
for(int j=0;p[j]*i<=n&&j<k;j++)
{
isprime[i*p[j]]=true;
if(i%p[j]==0) break;//欧拉筛核心
}
}
//筛法结束
for(int i=0;i<k;i++)
printf("%d ",p[i]);
return 0;
}
核心代码
接下来解释一下欧拉筛最为核心的一段代码
for(int j=0;p[j]*i<=n&&j<k;j++)
{
isprime[i*p[j]]=true;
if(i%p[j]==0) break;
}
在欧拉筛里,我们依然只筛除质数的倍数。由于已经打了一部分质数表,我们可以筛除p[j]i(p是质数表)。判断条件中的p[j]i<=n保证了筛除的数不会超出n的范围,节省时间也防止了数组下标向上越界,j<k保证j不超出当前质数表个数k。
第三行就是将素数的i倍筛除,标为true。
第四行是欧拉筛的核心也是最难理解的部分。我的个人理解就是我们让每个合数只被他的最小质因子筛除一次,如果i已经包含了p[j]这个因子,就有两种情况:
- i=p[j] ,此时p[j]一定是p中最后一个数,跳出循环
- i是p[j]的倍数,此时存在整数\(k=\frac{i}{p[j]}\),i可表示为\(k*p[j]\)。如果不跳出,我们下一个准备筛除的数就是\(p[j+1] *i\),即\(p[j+1]*k*p[j]\),这个数的最小质因子是p[j],不应被p[j+1]筛除。以此类推,之后筛除的所有数都有最小质因子p[j],不应被更大的质因子筛除,故跳出循环。
米勒罗宾素数检测法
基于随机算法,可以在O(logn)内判断一个数是否是素数,但存在一定的误差。
在需要判断的素数大小过大的时候,依靠素数筛法会爆内存,可以使用这种方法。
因为我没看懂就不具体写了,学会再回来更新。
标签:筛法,int,合数,29,素数,倍数,筛除 来源: https://www.cnblogs.com/kaixinqi/p/10367282.html