本系列文章将于2021年整理出版,书名《算法竞赛专题解析》。
前驱教材:《算法竞赛入门到进阶》 清华大学出版社
网购:京东 当当 想要一本作者签名书?点我
如有建议,请加QQ 群:567554289,或联系作者QQ:15512356
本文在公众号同步,阅读更方便。而且公众号有暑假福利:免费连载作者的书《胡说三国》
素数(质数)是数论的基础内容,本节介绍素数的判定。
( 如果读者学过一些数论,但是还没有系统读过初等数论的书,那么在阅读本文之前,最好也读一本。推荐《初等数论及其应用》,Kenneth H.Rosen著,夏鸿刚译,机械工业出版社,这本书很易读,非常适合学计算机的人阅读:1)概览并证明了初等数论的理论知识;2)理论知识的各种应用,可以直接用在算法题目里;3)大量例题和习题;4)与计算机算法编程有很多结合。花两天时间通读很有益处。)
关于素数,有以下有趣的事实:
(1)素数的数量有无限多。
(2)素数的分布。随着 x x x的增大,素数的分布越来越稀疏;第n个素数渐进于logn;随机整数 x x x是素数的概率是1/log x x 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 ≤ 1 0 12 10^{12} 1012时,用试除法;n > 1 0 12 10 ^{12} 1012时,用Miller_Rabin算法。
根据素数的定义,可以直接得到试除法:用[2, n-1]内的所有数去试着除n,如果都不能整除,就是素数。很容易发现,可以把[2, n-1]缩小到[2, n \sqrt n n ]。
试除法的复杂度是O( n \sqrt n n),n ≤ 1 0 12 10^{12} 1012时够用。下面是代码,注意for循环中对 n \sqrt n 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, n \sqrt n n]还可以继续缩小,如果提前算出范围内的所有素数,那么用这些素数来除n就行了。下一节的埃式筛法就用到这一原理。那么,范围[2, n \sqrt n n ]内有多少个素数?用 π ( x ) \pi(x) π(x)表示不超过整数 x x x的素数的个数,素数定理给出了素数密度的估计。
素数定理:随着 x x x的无限增长, π ( x ) \pi(x) π(x)和 x x x/log x x x的比趋于1,其中log取自然底数。
值得注意的是,有比 x x x/log x x x更好的近似,例如 L i ( x ) Li(x) Li(x)1 。
根据素数定理,一个随机整数 x x x是素数的概率是1/log x x x。
x x x等于1百万时,约有7.8万个质数; x x x等于1亿时,约有576万个质数。
如果n非常大,试除法就不够用了。例如poj 1811题, n < 2 54 2^{54} 254,如果用试除法, n = 2 27 ≈ 1 0 8 \sqrt n= 2^{27} ≈ 10^8 n=227≈108,提交到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, n \sqrt n n ]内找一些数去除n,如果都不能整除,那么n就有很大概率是个素数;尝试的次数越多,n是素数的概率就越大。这就是概率法素性测试的原理。
当然,数学家能想到更好的概率测试方法,例如费马素性测试、Miller_Rabin素性测试。后者是前者的升级版,应用最广泛。
判定一个整数是否为素数,称为素性测试(Primality test)。有确定型启发式算法和随机算法。随机算法有:费马(Fermat )素性测试、Solovay–Strassen素性测试、Miller-Rabin素性测试等。确定型启发式算法有AKS素性测试、Baillie–PSW素性测试等。
费马素性测试非常简单,它基于费马小定理。
费马小定理:设n是素数, a a a是正整数且与n互质,那么有 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1(mod \ n) an−1≡1(mod n)。
( 符号“ ≡ \equiv ≡”表示同余, c ≡ d ( m o d m ) c \equiv d(mod\ m) c≡d(mod m)的意思是c和d模m同余,例如6和16除以5,余数都是1。)
费马小定理的逆命题也几乎成立。费马素性测试,就是基于费马小定理的逆命题,下面介绍方法。
为了测试n是否为素数,在1~n之间任选一个随机的基值 a a a,注意 a a a并不需要与n互质:
(1)如果 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1(mod\ n) an−1≡1(mod n)不成立,那么n肯定不是素数。这实际上是费马小定理的逆否命题。
(2)如果 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1(mod\ n) an−1≡1(mod n)成立,那么n很大概率是素数,尝试的 a a a越多,n是素数的概率越大。称n是一个基于 a a a的伪素数。
可惜的是,从(2)可以看出费马素性测试并不是完全正确的。对某个 a a a值,总有一些合数被误判而通过了测试;不同的 a a a值,被误判的合数不太一样。特别地,有一些合数,不管选什么 a a a值,都能通过测试。这种数叫做Carmichael数,前三个数是561、1105、1729。不过,Carmichael数很少,前1亿个正整数中只有255个。而且当n趋向无穷时,Carmichael数的分布极为稀疏,费马素性测试几乎不会出错,所以它是一种相当好的方法。
费马素性测试的编码非常简单。其中的关键是计算 a n − 1 a^{n-1} an−1,这是一个很大的数,不能直接算,需要用快速幂2来编码,后面的hdu 2138题给出了代码。
费马素性测试的缺点是不能排除Carmichael数。把费马素性测试稍微改进一下,就是Miller-Rabin素性测试算法。Miller-Rabin素性测试的原理概况地说是这样:用费马测试排除掉非Carmichael数,而大部分Carmichael数用下面介绍的推论排除。
(1)Miller-Rabin算法用到的推论
这个推论和一个数论定理有关。
定理3:如果p是一个奇素数,且e≥1,则方程 x 2 ≡ 1 ( m o d p e ) x^2 \equiv 1(mod \ p^e) x2≡1(mod pe),仅有两个解: x x x = 1和 x x x = -1。
当e = 1时,方程仅有两个解 x x x = 1和 x x x = p-1。
证明: x 2 ≡ 1 ( m o d p ) x^2 \equiv 1(mod \ p) x2≡1(mod p)等价于 x 2 − 1 ≡ 0 ( m o d p ) x^2 -1\equiv 0(mod \ p) x2−1≡0(mod p),即 ( x + 1 ) ( x − 1 ) ≡ 0 ( m o d p ) (x+1)(x-1)\equiv 0(mod \ p) (x+1)(x−1)≡0(mod p),那么或者 x x x-1能被p整除,此时 x x x = 1,或者 x x x+1能被p整除,此时 x x x = p-1。
把 x x x = 1和 x x x = p-1称为“ x x x对模p来说1的平凡平方根”。说法有点儿拗口,理解它的意思就好了。
Miller-Rabin素性测试用到这个方程: x 2 ≡ 1 ( m o d n ) x^2 \equiv 1(mod \ n) x2≡1(mod n)。如果一个数 x x x满足方程 x 2 ≡ 1 ( m o d n ) x^2 \equiv 1(mod \ n) x2≡1(mod n),但 x x x不等于平凡平方根1或n-1,那么称 x x x是对模n来说1的“非平凡”平方根。例如, x x x=6,n=35,6是对模35来说1的非平凡平方根。
下面给出定理的推论。
推论:如果对模n存在1的非平凡平方根,则n是合数。
推论是定理的逆否命题,即如果对n存在1的非平凡平方根,则n不可能是奇素数或者奇素数的幂。
(2)Miller-Rabin素性测试的步骤
输入n>2,且n是奇数,测试它是否为素数。
根据费马测试,如果 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1(mod \ n) an−1≡1(mod n)不成立,那么n肯定不是素数。
令 n − 1 = 2 t u n-1 = 2^tu n−1=2tu,其中u是奇数,t是正整数。编码的时候可以这样做:n-1的二进制表示,是奇数u的二进制表示,后面加t个零。选一个随机的基值 a a a,有:
a n − 1 ≡ ( a u ) 2 t ( m o d n ) a^{n-1} \equiv (a^u)^{2^t}(mod \ n) an−1≡(au)2t(mod n)
为了计算 a n − 1 m o d n a^{n-1} mod \ n an−1mod n,可以先算出 a u m o d n a^u mod \ n aumod n,然后对结果连续平方t次取模。这是因为符合乘法模运算规则: ( c ∗ d ) m o d n = ( c m o d n ∗ d m o d n ) m o d n (c*d) mod \ n = (c\ mod \ n *d\ mod \ n) mod \ n (c∗d)mod n=(c mod n∗d mod n)mod n。
在计算过程中,做以下判断:
1)模运算结果不是1,即 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1(mod \ n) an−1≡1(mod n)不成立,根据费马测试,断定n是合数。
2)模运算结果是1,但是发现了1的非平凡平方根,根据推论,断定n是合数。
以Carmichael数n = 561为例,演示计算过程。n-1 = 2 4 2^4 24×35,u = 35,t = 4,选 a a a = 7,计算过程是:
1) a u m o d n a^u mod \ n aumod n = 7 35 ^{35} 35 mod 561 = 241
2)241 2 ^2 2 mod 561 = 298
3)298 2 ^2 2 mod 561 = 166
4)166 2 ^2 2 mod 561 = 67
5)67 2 ^2 2 mod 561 = 1
在最后一步,67 2 ^2 2 mod 561 = 1符合费马测试,但是出现了67这个非平凡平方根,不符合推论。这个例子说明:费马测试不能发现的Carmichael数,用Miller-Rabin测试能找到。
(3)Miller-Rabin算法的出错率和计算复杂度
Miller-Rabin算法需要用多个随机的基值 a a a来做以上的测试。设有s个 a a a,共做s次测试,出错的概率是2 − s ^{-s} −s。当s = 50时,出错概率已经小到可以忽略不计了。
计算复杂度:算法做了s次模取幂运算,总复杂度是O(slogn)。
(4)编码
根据以上讨论,Miller-Rabin算法的编码包括4个内容:费马小定理、二次探测定理(推论)、乘法模运算、快速幂取模。
下面给出hdu 2138的代码,它完全重现了上面的解释,请对照理解。
#include
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\n",cnt);
}
return 0;
}
前面给出的c代码,变量最大是64位的long long类型,约 1 0 19 10^{19} 1019,如果更大,就需要自己处理高精度大数了。
大学的程序设计竞赛,可以用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
《初等数论及其应用》,Kenneth H.Rosen著,夏鸿刚译,机械工业出版社,57页给出了 L i ( x ) Li(x) Li(x)的定义,60页给出了 π ( x ) \pi(x) π(x)表格。 ↩︎
《算法竞赛入门到进阶》清华大学出版社,罗勇军,郭卫斌著,156页,详细介绍了快速幂的原理和编码。] ↩︎
《算法导论》Thomas H.Cormen等著,潘金贵等译,机械工业出版社,539页,定理31.34、推论31.35,并给出了证明。这个定理有人称为“二次探测定理”。 ↩︎
代码参考:https://blog.csdn.net/qingshui23/article/details/51456944。 ↩︎