其他分享
首页 > 其他分享> > 素数筛法

素数筛法

作者:互联网

这篇博客是按照我学习素数的顺序写的,算法也是一步步推进,一步步优化的,想找最高效的算法可以直接翻到欧拉筛。这是我的第一篇博客,欢迎各路大神指正错误以及提供建议。

素数定义

质数又称素数。一个大于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以内的素数(加粗代表新划去的数):

  1. 建立集合
    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. 筛除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. 筛除3的倍数:6 9 12 15 18 21 24 27 30
    2 3 5 7 11 13 17 19 23 25 29
  4. 4为合数跳过4(之后合数省略)
  5. 筛除5的倍数:10 15 20 25 30
    2 3 5 7 11 13 17 19 23 29
  6. 筛除7的倍数:14 21 28
    2 3 5 7 11 13 17 19 23 29
  7. 筛除11的倍数:22
    2 3 5 7 11 13 17 19 23 29
  8. 筛除13的倍数:26
    2 3 5 7 11 13 17 19 23 29
  9. 筛除17、19、23、29的倍数:均不含小于30的倍数
    ... ...
  10. 筛除到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]这个因子,就有两种情况:

  1. i=p[j] ,此时p[j]一定是p中最后一个数,跳出循环
  2. 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