质数筛法详解

理论及实现

定义:

若一个正整数无法被除了1和它本身的之外的任何自然数整除,则称该为质数(素数),否则称该正整数为合数。


判定方法

试除法

引理:

若一个正整数\(N\)为合数,则存在一个能整除\(N\)的数\(T\)\(2≤T≤ \sqrt N\)

证明就不再赘述,读者可以自行验证:

因此,我们只需要枚举\(2-\sqrt N\)。只要这之中的所有数都不能被\(N\)整除,那么\(N\)就是质数了:

    #include<cmath>

    bool is_prime(int n) {
        if(n < 2) return false;
        for(int i = 2; i <= sqrt(n); i++)
            if(n % i == 0) return false;
        return true;
    }

    //调用 (自定义方式) 
    if(is_prime(x))...; 

    bool flag = is_prime(x); 

Eratosthenes 筛法

基本思想:

任意整数 x 的倍数 2x、3x…,都不是质数。

根据质数的定义,上述命题显然成立。

那么我们可以从\(2\)开始,由小到大扫描每个数\(x\),把它的倍数(\(2x\)\(3x…\) \(N/x\)(向下取整)\(*x\) )全部标记为合数。 若扫描到一个数时,它尚未被标记,则它一定是质数。

并且\(Eratosthenes\) 筛法还有可以优化的地方,比如2与3都会标记到6。其实我们可以发现,小于\(x^2\)的数其实都已经被其他书标记过了,因此我们可以优化,每次从\(x^2\)开始扫描即可。

    #include<cstring>
    #include<cstdio>


    bool v[SIZE];
    void primes(int n) {//n为范围 
        memset(v,0,sizeof(v));
        for(int i = 2; i <= n; i++) {
            if(v[i]) continue;
            printf("%d", i);
            for(int j = i; j <= n/i; j++)
                v[i * j] = 1;
        }
    }
    //调用:
    primes(n);

\(Eratosthenes\) 筛法的时间复杂度为\(O(N\log \log N)\),已经非常接近线性。


线性筛法

即使在\(Eratosthenes\) 筛法优化后也会重复标记,这样就造成了时间的浪费,比如说\(2\)\(3\)都会标记\(12\)

线性筛法思想:

通过“从大到小累积质因子”的方式标记合数,即12只有322一种标记方式。

对于下面两步有两种办法

  • 第一种:
  1. 依次考虑\(2-N\)之间的每一个数\(i\)

  2. \(v[i]=0\),说明\(i\)是质数,把\(i\)保存下来。

  3. 扫描不大于\(v[i]\)的每个质数\(p\),令\(v[i*p]\)=1。

  4. 在扫描过程中,记录筛过的质数总数为\(num\),且要保证\(prime[j],j≤num\)为合数\(i*prime[j]\)的最小质因子,在找到一个\(i\)满足\(i\) % \(prime[j]=0\),即比一个合数大的质数和该合数的乘积可用一个更大的合数和比其小的质数相乘得到,那么就退出标记。

  • 第二种(《算法竞赛进阶指南》)
  1. 依次考虑\(2-N\)之间的每一个数\(i\)

  2. \(v[i]=x\),说明\(x\)是质数,把\(x\)保存下来。

  3. 扫描不大于\(v[i]\)的每个质数\(p\),令\(v[i*p]=p\)。也就是在\(i\)的基础上累积一个质因子\(p\)。因为\(p≤v[i]\),所以\(p\)就是合数\(i*p\)的最小质因子。

以上两种方法都保证合数\(i*p\)只会被它最小的质因子筛一次,时间复杂度为\(O(N)\),已经达到了线性。

   //第一种方法: 
   #include<cstring>
   #include<cstdio>

   int v[MAX_N], prime[MAX_N], num;
   void primes(int n) {
       memset(v, 0, sizeof(v));
       num = 0;
       for(int i = 2; i <= n; i++) {
           if(v[i] == 0) prime[++num] = i;
           for(int j = 1; j <= num && i * prime[j] <= n; j++) {
               v[i * prime[j]] = 1;
               if(i % prime[j] == 0) break;
           }		
       }
   }
   //调用:看自己使用方式,下面为判定质数
   primes(n);
   
   if(v[x]) printf("NO");
   else printf("YES"); 


   //第二种方法
   #include<cstring>
   #include<cstdio>

   int v[MAX_N], prime[MAX_N], num;
   void primes(int n) {
       memset(v, 0, sizeof(v));
       num = 0;
       for(int i = 2; i <= n; i++) {
           if(v[i] == 0) v[i] = i, prime[++num] = i;
           for(int j = 1; j <= num; j++) {
               if(prime[j] > v[i] || prime[j] > n / i) break;
               v[i * prime[j]] = prime[j];
           }		
       }
   }
   //调用:看自己使用方式,下面为判定质数
   primes(n);
   
   if(v[x] == x) printf("YES");
   else printf("NO"); 

“6倍数”筛法

当然,质数筛法还不止这些,我在网上查阅到了这种“6筛法”。

  • 引理

大于等于5的质数都分布在6的倍数的两侧。

但需要强调的是,分布在\(6\)的倍数的两侧的数不一定是质数:如 \(4*6=24\),但\(25\)不是质数。

  • 证明(反证法):

\(x≥1,x∈Z\),将大于等于\(5\)的自然数表示为

\(6x-1,6x,6x+1,6x+2…6(x+1),6(x+1)+1…\)

若大于等于\(5\)质数\(p\)不在\(6\)的倍数的两侧,即\(p\)不能表示为\(6i-1\)\(6i+1,i=1,2,3…\)那么质数就一定表示为\(6i+2\)\(6i+3\)\(6i+4,i=1,2,3…\)其中的一种,但是很明显的\(6i+2\)
\(6i+4\)\(2\)的倍数,\(6i+3\)为3的倍数,根据质数定义,则\(p\)一定不是质数,与假设矛盾,故原命题成立。


对于“试除法”,我们根据上面的定理可以将 \(i++\) 改为\(i+=6\),并且\(i\)的初值设为\(5\)。每次判定 一个数\(x\)是否能整除 \(i\)或是\(i+2,2≤i≤sqrt(n),i=1,2,3…\),若是,则该数不是质数;否则,运行完后,没有返回f\(alse\),则该数是质数,返回\(true\)

  • 这里对为什么是整除\(i,i+2\)做一个说明:\(i\)初值从\(5\)开始,每次加\(6\),始终是\(6\)的倍数左侧的那个数,即\(i=6x-1,x=1,2,3…\),所以每次对\(i\)\(i+2\)判定。
    #include<cmath>

    bool is_prime(int x) {
        if(x == 1 || x == 4) return false;
        if(x == 2 || x == 3) return true;
        //以上对小于5的数进行处理 
        if(x % 6 != 1 || x % 6 != 5) return false;
        //不在6的倍数的两侧(不能用6x-1或6x+1表示) 
        for(int i = 5; i <= sqrt(x); i += 6) 
            if(x % i == 0 || x % (i + 2) == 0)
                return false;
        //在6两侧的不一定是质数,用6x-1和6x+1判定即可
        //类似于用质数标记合数的思想  
        return true; 
    }
    //调用:(判定质数) 
    if(is_prime(x)) printf("YES");
    else printf("NO"); 

    //记录质数(O(N*sqrt(N)/6) 
    int num, v[MAX_N]; 
    if(is_prime(i)) v[++num] = i//i = 1, 2 ... N

*章末练习

  • 以下题目按难度排序(带“+”题目为选做题目):
  1. P3383 【模板】线性筛素数

  2. P1865 A % B Problem

  3. P2563 [AHOI2001]质数和分解

  4. P2926 [USACO08DEC]拍头Patting Heads

  5. P1313 计算系数

  6. P1069 细胞分裂

  7. P3737 [HAOI2014]遥感监测

  8. +P2044 [NOI2012]随机数生成器

  9. P2455 [SDOI2006]线性方程组

  10. +P2924 [USACO08DEC]大栅栏Largest Fence

  11. P4446 [AHOI2018初中组]根式化简

  12. P3200 [HNOI2009]有趣的数列

  13. P2568 GCD

  14. +P4208 [JSOI2008]最小生成树计数

  15. +P2485 [SDOI2011]计算器

  16. +P3702 [SDOI2017]序列计数

  17. +P4068 [SDOI2016]数字配对

  18. +P4359 [CQOI2016]伪光滑数

  19. P3260 [JLOI2014]镜面通道

  20. +P2150 [NOI2015]寿司晚宴

  21. +P3747 [六省联考2017]相逢是问候

  22. +P4191 [CTSC2010]性能优化

  23. P4607 [SDOI2018]反回文串

  • 以上较难的题目可能需要的知识储备:

乘法逆元,逆元、组合数学、卡特兰、矩阵运算、高斯消元、生成树、左偏树、哈希、容斥、快速傅里叶变,DFT,FFT、前缀和、块状链表,块状数组,分块、同余,中国剩余定理、动态规划,动规,dp、状态压缩、分治、线段树,以及一系列其他的基本操作(如搜索)、数论知识(如\(gcd\)质因数分解)


写在后面

下面为题目P3383 【模板】线性筛素数的一些评测记录(无(\(O_2\))):

1. 朴素算法,时间复杂度\(O(sqrt(N)M),AC:\)朴素算法

2. “6倍数”优化朴素算法,时间复杂度\(O(sqprt(N)M/6),AC:\)“6倍数”筛法

3. \(Eratosthenes\) 筛法,时间复杂度\(O(NloglogN),AC:\)Eratosthenes 筛法

4. 线性筛,时间复杂度\(O(N),AC:\)线性筛1&线性筛2

  • \(PS:\)

1. 网络波动可能导致评测结果不够准确,以理论时空复杂度为准。

2. 由于测试数据\(N<=10000000,M<=100000\)\(M\)远小于\(N\),导致朴素算法和优化的“6倍数”筛法看起来比 \(Eratosthenes\) 筛法和线性筛法快,实际上是后者在大数据上远快于前者。

3. 部分思想及材料参考李煜东《算法竞赛进阶指南》

4. "6倍数"筛法原文来自于:判断一个数是否为质数/素数——从普通判断算法到高效判断算法思路

posted @ 2019-09-22 13:41  Ning-H  阅读(749)  评论(0编辑  收藏  举报