zoukankan      html  css  js  c++  java
  • 算法竞赛专题解析(18):数论--素数的判定

    本系列文章将于2021年整理出版,书名《算法竞赛专题解析》。
    前驱教材:《算法竞赛入门到进阶》 清华大学出版社
    网购:京东 当当      想要一本作者签名书?点我
    如有建议,请加QQ 群:567554289,或联系作者QQ:15512356
    本文在公众号同步,阅读更方便:算法专辑
    公众号还有暑假福利,免费连载作者的书:胡说三国

       素数(质数)是数论的基础内容,本节介绍素数的判定。
      ( 如果读者学过一些数论,但是还没有系统读过初等数论的书,那么在阅读本文之前,最好也读一本。推荐《初等数论及其应用》,Kenneth H.Rosen著,夏鸿刚译,机械工业出版社,这本书很易读,非常适合学计算机的人阅读:1)概览并证明了初等数论的理论知识;2)理论知识的各种应用,可以直接用在算法题目里;3)大量例题和习题;4)与计算机算法编程有很多结合。花两天时间通读很有益处。)
       关于素数,有以下有趣的事实:
       (1)素数的数量有无限多。
       (2)素数的分布。随着(x)的增大,素数的分布越来越稀疏;第n个素数渐进于logn;随机整数(x)是素数的概率是1/log (x)
       (3)对于任意正整数n,存在至少n个连续的正合数。
       有大量关于素数的猜想,著名的有:
       (1)波特兰猜想。对任意给定的正整数n > 1,存在一个素数p,使得n < p < 2n。已经证明。
       (2)孪生素数猜想。存在无穷多的形如p和p+2的素数对。
       (3)素数等差数列猜想。对任意正整数n >2,有一个由素数组成的长度为n的等差数列。
       (4)哥德巴赫猜想。每个大于2的正偶数可以写成两个素数的和。这是最有名的素数猜想,也是最令人头疼的猜想,已经困扰数学家3世纪。至今为止,最好的结果仍然是陈景润1966年做出的

    一、小素数的判定

       素数定义:只能被1和自己整除的数。
       判定一个数是否为素数,有重要的工程意义。在密码学中,经常用到数百位的超大的素数。但是,直接生成一个大素数几乎是不可能的,只能用测试法来找到素数,也就是给定某个范围,然后测试其中哪些是素数。
       如何判断一个数n是不是素数?当n ≤ (10^{12})时,用试除法;n >(10 ^{12})时,用Miller_Rabin算法。
       根据素数的定义,可以直接得到试除法:用[2, n-1]内的所有数去试着除n,如果都不能整除,就是素数。很容易发现,可以把[2, n-1]缩小到[2,(sqrt n) ]。
       试除法的复杂度是O((sqrt n)),n ≤(10^{12})时够用。下面是代码,注意for循环中对(sqrt n)的处理。

    bool is_prime(long long n){
         if(n <= 1)   return false;             //1不是质数
         for(long long i=2; i*i <= n; i++)      //不要这样写:i <= sqrt(n)
             if(n % i == 0)  return false;      //能整除,不是质数
         return true;
    }
    

       范围[2,(sqrt n)]还可以继续缩小,如果提前算出范围内的所有素数,那么用这些素数来除n就行了。下一节的埃式筛法就用到这一原理。那么,范围[2,(sqrt n) ]内有多少个素数?用(pi(x))表示不超过整数(x)的素数的个数,素数定理给出了素数密度的估计。
       素数定理:随着(x)的无限增长,(pi(x))(x)/log (x)的比趋于1,其中log取自然底数。
       值得注意的是,有比(x)/log (x)更好的近似,例如(Li(x))[1]

       根据素数定理,一个随机整数(x)是素数的概率是1/log (x)
       (x)等于1百万时,约有7.8万个质数;(x)等于1亿时,约有576万个质数。

    二、大素数的判定

       如果n非常大,试除法就不够用了。例如poj 1811题, n < (2^{54}),如果用试除法,(sqrt n= 2^{27} ≈ 10^8),提交到OJ会超时。即使n不大,但是如果要检查很多个n,总时间也会超时,例如hdu 2138题。
       (《算法导论》Thomas H.Cormen等著,潘金贵等译,机械工业出版社,544页,31.8节“素数的测试”。31.8节的叙述非常清晰易懂,本文的理论内容改写自这一节。)


    How many prime numbers hdu 2138
    题目描述:给你很多很多正整数,统计其中素数的个数。
    输入格式:有很多测试。每个测试的第一行是整数的个数,第二行是整数。
    输出格式:对于每个测试,输出素数的个数。
    样例输入
    3
    2 3 4
    样例输出
    2


      大素数的判定,目前并没有快速的确定性算法。那么,有没有很快的方法,能“差不多”判定一个极大的整数n是素数呢?从试除法得到提示,读者可以想到一个“取巧”的办法:在[2,(sqrt n) ]内找一些数去除n,如果都不能整除,那么n就有很大概率是个素数;尝试的次数越多,n是素数的概率就越大。这就是概率法素性测试的原理。
      当然,数学家能想到更好的概率测试方法,例如费马素性测试、Miller_Rabin素性测试。后者是前者的升级版,应用最广泛。
      判定一个整数是否为素数,称为素性测试(Primality test)。有确定型启发式算法和随机算法。随机算法有:费马(Fermat )素性测试、Solovay–Strassen素性测试、Miller-Rabin素性测试等。确定型启发式算法有AKS素性测试、Baillie–PSW素性测试等。

    1、费马素性测试

      费马素性测试非常简单,它基于费马小定理。
      费马小定理:设n是素数,(a)是正整数且与n互质,那么有(a^{n-1} equiv 1(mod n))

      ( 符号“ (equiv)”表示同余,(c equiv d(mod m))的意思是c和d模m同余,例如6和16除以5,余数都是1。)
      费马小定理的逆命题也几乎成立。费马素性测试,就是基于费马小定理的逆命题,下面介绍方法。
      为了测试n是否为素数,在1~n之间任选一个随机的基值(a),注意(a)并不需要与n互质:
      (1)如果(a^{n-1} equiv 1(mod n))不成立,那么n肯定不是素数。这实际上是费马小定理的逆否命题。
      (2)如果(a^{n-1} equiv 1(mod n))成立,那么n很大概率是素数,尝试的(a)越多,n是素数的概率越大。称n是一个基于(a)伪素数
      可惜的是,从(2)可以看出费马素性测试并不是完全正确的。对某个(a)值,总有一些合数被误判而通过了测试;不同的(a)值,被误判的合数不太一样。特别地,有一些合数,不管选什么(a)值,都能通过测试。这种数叫做Carmichael数,前三个数是561、1105、1729。不过,Carmichael数很少,前1亿个正整数中只有255个。而且当n趋向无穷时,Carmichael数的分布极为稀疏,费马素性测试几乎不会出错,所以它是一种相当好的方法。
      费马素性测试的编码非常简单。其中的关键是计算(a^{n-1}),这是一个很大的数,不能直接算,需要用快速幂[2]来编码,后面的hdu 2138题给出了代码。

    2、Miller-Rabin素性测试

      费马素性测试的缺点是不能排除Carmichael数。把费马素性测试稍微改进一下,就是Miller-Rabin素性测试算法。Miller-Rabin素性测试的原理概况地说是这样:用费马测试排除掉非Carmichael数,而大部分Carmichael数用下面介绍的推论排除。
    (1)Miller-Rabin算法用到的推论
      这个推论和一个数论定理有关。
      定理[3]:如果p是一个奇素数,且e≥1,则方程(x^2 equiv 1(mod p^e)),仅有两个解:(x) = 1和(x) = -1。
      当e = 1时,方程仅有两个解(x) = 1和(x) = p-1。
      证明:(x^2 equiv 1(mod p))等价于(x^2 -1equiv 0(mod p)),即((x+1)(x-1)equiv 0(mod p)),那么或者(x)-1能被p整除,此时(x) = 1,或者 (x)+1能被p整除,此时(x) = p-1。

      把(x) = 1和(x) = p-1称为“(x)对模p来说1的平凡平方根”。说法有点儿拗口,理解它的意思就好了。
      Miller-Rabin素性测试用到这个方程:(x^2 equiv 1(mod n))。如果一个数(x)满足方程(x^2 equiv 1(mod n)),但(x)不等于平凡平方根1或n-1,那么称(x)是对模n来说1的“非平凡”平方根。例如,(x)=6,n=35,6是对模35来说1的非平凡平方根。
      下面给出定理的推论。
      推论:如果对模n存在1的非平凡平方根,则n是合数。
      推论是定理的逆否命题,即如果对n存在1的非平凡平方根,则n不可能是奇素数或者奇素数的幂。
    (2)Miller-Rabin素性测试的步骤
       输入n>2,且n是奇数,测试它是否为素数。
       根据费马测试,如果(a^{n-1} equiv 1(mod n))不成立,那么n肯定不是素数。
      令 (n-1 = 2^tu),其中u是奇数,t是正整数。编码的时候可以这样做:n-1的二进制表示,是奇数u的二进制表示,后面加t个零。选一个随机的基值(a),有:
        (a^{n-1} equiv (a^u)^{2^t}(mod n))
      为了计算(a^{n-1} mod n),可以先算出(a^u mod n),然后对结果连续平方t次取模。这是因为符合乘法模运算规则:((c*d) mod n = (c mod n *d mod n) mod n)
      在计算过程中,做以下判断:
      1)模运算结果不是1,即(a^{n-1} equiv 1(mod n))不成立,根据费马测试,断定n是合数。
      2)模运算结果是1,但是发现了1的非平凡平方根,根据推论,断定n是合数。
      以Carmichael数n = 561为例,演示计算过程。n-1 = (2^4)×35,u = 35,t = 4,选(a) = 7,计算过程是:
      1)(a^u mod n) = 7(^{35}) mod 561 = 241
      2)241(^2) mod 561 = 298
      3)298(^2) mod 561 = 166
      4)166(^2) mod 561 = 67
      5)67(^2) mod 561 = 1
      在最后一步,67(^2) mod 561 = 1符合费马测试,但是出现了67这个非平凡平方根,不符合推论。这个例子说明:费马测试不能发现的Carmichael数,用Miller-Rabin测试能找到。
    (3)Miller-Rabin算法的出错率和计算复杂度
      Miller-Rabin算法需要用多个随机的基值(a)来做以上的测试。设有s个(a),共做s次测试,出错的概率是2(^{-s})。当s = 50时,出错概率已经小到可以忽略不计了。
      计算复杂度:算法做了s次模取幂运算,总复杂度是O(slogn)。
    (4)编码
      根据以上讨论,Miller-Rabin算法的编码包括4个内容:费马小定理、二次探测定理(推论)、乘法模运算、快速幂取模。
      下面给出hdu 2138的代码,它完全重现了上面的解释,请对照理解。

    #include <bits/stdc++.h>
    typedef long long LL;
    LL fast_pow(LL x,LL y,int m){   //快速幂取模:x^y mod m
        LL res = 1;
        while(y) {
            if(y&1) res*=x, res%=m;
            x*=x, x%=m;
            y>>=1;
        }
        return res;
    }
    
    bool witness(LL a, LL n){       // Miller-Rabin素性测试。返回true表示n是合数
            LL u = n-1; 
            int t = 0;              // n-1的二进制,是奇数u的二进制,后面加t个零
            while(u&1 ==0) u = u>>1, t++;    // 整数n-1末尾有几个0,就是t
            LL x1, x2;
            x1 = fast_pow(a,u,n);            // 先计算  a^u mod n
            
            for(int i=1; i<=t; i++) {        // 做t次平方取模
                x2 = fast_pow(x1,2,n);       // x1^2 mod n
                if(x2 == 1 && x1 != 1 && x1 != n-1) return true;  //用推论判断
                x1 = x2;
            }
            if(x1 != 1) return true;         //最后用费马测试判断是否为合数
            return false;
    }
    
    int miller_rabin(LL n,int s){            //对n做s次测试
        if(n<2)  return 0;  
        if(n==2) return 1;                   //2是素数
        if(n % 2 == 0 ) return 0;            //偶数
    
    	for(int i = 0;i < s && i < n;i++){   //做s次测试
    		LL a = rand() % (n - 1) + 1;     //基值a是随机数
     		if(witness(a,n))  return 0;      //n是合数,返回0           	              
    	}
     	return 1;                            //n是素数,返回1
    }
    
    int main(){
    	int m;                   
    	while(scanf("%d",&m) != EOF){
    		int cnt = 0;
     		for(int i = 0; i < m; i++){
    			LL n; scanf("%lld",&n);   
                int s = 50;               //做s次测试
           	    cnt += miller_rabin(n,s);
    		} 
    		printf("%d
    ",cnt);
    	} 
    	return 0;
    }
    

    三、用java函数判定大素数

      前面给出的c代码,变量最大是64位的long long类型,约(10^{19}),如果更大,就需要自己处理高精度大数了。
      大学的程序设计竞赛,可以用java编码。java有函数可以直接判定一个数是否为素数,这个函数是isProbablePrime(),它的内部实现用到了Miller-Rabin测试和Lucas-Lehmer测试。
      编码极其简单,不用自己处理大数的输入,也不用自己写算法。下面是java代码[4]。连续读入数字,如果是素数,就输出“Yes”。

    import java.math.*;
    import java.util.*;
    public class Main {
        public static void main(String[] args){
            Scanner in = new Scanner (System.in);
            BigInteger a;
            while(in.hasNextBigInteger()){
                a = in.nextBigInteger();
                if(a.isProbablePrime(1))
                    System.out.println("Yes");
                else
                    System.out.println("No");
            }
        }
    }
    

      读者可以用上述代码验证一个100位的素数:
      9149014901591490015900000003849002684902869159002693938590001590003839159149015901392684902859014901


    1. 《初等数论及其应用》,Kenneth H.Rosen著,夏鸿刚译,机械工业出版社,57页给出了(Li(x))的定义,60页给出了(pi(x))表格。 ↩︎

    2. 《算法竞赛入门到进阶》清华大学出版社,罗勇军,郭卫斌著,156页,详细介绍了快速幂的原理和编码。] ↩︎

    3. 《算法导论》Thomas H.Cormen等著,潘金贵等译,机械工业出版社,539页,定理31.34、推论31.35,并给出了证明。这个定理有人称为“二次探测定理”。 ↩︎

    4. 代码参考:https://blog.csdn.net/qingshui23/article/details/51456944↩︎

  • 相关阅读:
    js笔记——js里var与变量提升
    微信授权机制
    HTML5的sessionStorage和localStorage
    博客园主题响应式布局
    [转]五种开源协议的比较(BSD,Apache,GPL,LGPL,MIT)
    搭建jekyll博客
    Oracle VM VirtualBox技巧
    jQuery实现全选、全不选、反选
    js里slice,substr和substring的区别
    js里cookie操作
  • 原文地址:https://www.cnblogs.com/luoyj/p/13394953.html
Copyright © 2011-2022 走看看