zoukankan      html  css  js  c++  java
  • 素数筛法

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

    素数定义

    质数又称素数。一个大于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)O(a)

    初步优化

    内容

    实际上,我们只需要枚举asqrt a次即可完成判断,可将时间复杂度优化到O(a)O(sqrt a)

    原理

    利用反证法,假设一个数a,在2到asqrt a之间没有找到a的因数,却在a+1sqrt a+1a1a-1之间找到一个a的因数i,则必存在a的另一个因数j=aij=frac{a}{i}。因为i&gt;ai&gt;sqrt a,可得1i&lt;aafrac{1}{i}&lt;frac{sqrt a}{a},可得j=ai&lt;aj=frac{a}{i}&lt;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(n)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开始没有筛除掉任何新的素数,因此我们可以进行优化。
    事实上我们只需要枚举到nsqrt 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=ip[j]k=frac{i}{p[j]},i可表示为kp[j]k*p[j]。如果不跳出,我们下一个准备筛除的数就是p[j+1]ip[j+1] *i,即p[j+1]kp[j]p[j+1]*k*p[j],这个数的最小质因子是p[j],不应被p[j+1]筛除。以此类推,之后筛除的所有数都有最小质因子p[j],不应被更大的质因子筛除,故跳出循环。

    米勒罗宾素数检测法

    基于随机算法,可以在O(logn)内判断一个数是否是素数,但存在一定的误差。
    在需要判断的素数大小过大的时候,依靠素数筛法会爆内存,可以使用这种方法。
    因为我没看懂就不具体写了,学会再回来更新。

  • 相关阅读:
    Java 线程之间的通讯,等待唤醒机制
    Java 死锁以及死锁的产生
    2018Java开发面经(持续更新)
    OpenFlow1.3协议wireshark抓包分析
    SDN核心技术剖析和实战指南---读书笔记
    Kafka常用命令
    安装kafka+zk-ui
    flink窗口
    flink架构原理
    安装Flink集群
  • 原文地址:https://www.cnblogs.com/kaixinqi/p/10371471.html
Copyright © 2011-2022 走看看