小Hi和小Ho最近突然对密码学产生了兴趣,其中有个叫RSA的公钥密码算法。RSA算法的计算过程中,需要找一些很大的质数。
小Ho:要如何来找出足够大的质数呢?
小Hi:我倒是有一个想法,我们可以先随机一个特别大的初始奇数,然后检查它是不是质数,如果不是就找比它大2的数,一直重复,直到找到一个质数为止。
小Ho:这样好像可行,那我就这么办吧。
过了一会儿,小Ho拿来了一张写满数字的纸条。
小Ho:我用程序随机生成了一些初始数字,但是要求解它们是不是质数太花时间了。
小Hi:你是怎么做的啊?
说着小Hi接过了小Ho的纸条。
小Ho:比如说我要检测数字n是不是质数吧,我就从2开始枚举,一直到sqrt(n),看能否被n整除。
小Hi:那就对了。你看纸条上很多数字都是在15、16位左右,就算开方之后,也有7、8位的数字。对于这样大一个数字的循环,显然会很花费时间。
小Ho:那有什么更快速的方法么?
小Hi:当然有了,有一种叫做Miller-Rabin质数测试的算法,可以很快的判定一个大数是否是质数。
提示:Miller-Rabin质数测试
第1行:1个正整数t,表示数字的个数,10≤t≤50
第2..t+1行:每行1个正整数,第i+1行表示正整数a[i],2≤a[i]≤10^18
第1..t行:每行1个字符串,若a[i]为质数,第i行输出"Yes",否则输出"No"
样例输出
Yes
Yes
No
提示:Miller-Rabin质数测试
小Hi:这种质数算法是基于费马小定理的一个扩展,首先我们要知道什么是费马小定理:
费马小定理:对于质数p和任意整数a,有a^p ≡ a(mod p)(同余)。反之,若满足a^p ≡ a(mod p),p也有很大概率为质数。
将两边同时约去一个a,则有a^(p-1) ≡ 1(mod p)
也即是说:假设我们要测试n是否为质数。我们可以随机选取一个数a,然后计算a^(n-1) mod n,如果结果不为1,我们可以100%断定n不是质数。
否则我们再随机选取一个新的数a进行测试。如此反复多次,如果每次结果都是1,我们就假定n是质数。
该测试被称为Fermat测试。需要注意的是:Fermat测试不一定是准确的,有可能出现把合数误判为质数的情况。
Miller和Rabin在Fermat测试上,建立了Miller-Rabin质数测试算法。
与Fermat测试相比,增加了一个二次探测定理:
如果p是奇素数,则 x^2 ≡ 1(mod p)的解为 x ≡ 1 或 x ≡ p - 1(mod p)
如果a^(n-1) ≡ 1 (mod n)成立,Miller-Rabin算法不是立即找另一个a进行测试,而是看n-1是不是偶数。如果n-1是偶数,另u=(n-1)/2,并检查是否满足二次探测定理即a^u ≡ 1 或 a^u ≡ n - 1(mod n)。
举个Matrix67 Blog上的例子,假设n=341,我们选取的a=2。则第一次测试时,2^340 mod 341=1。由于340是偶数,因此我们检查2^170,得到2^170 mod 341=1,满足二次探测定理。同时由于170还是偶数,因此我们进一步检查2^85 mod 341=32。此时不满足二次探测定理,因此可以判定341不为质数。
将这两条定理合起来,也就是最常见的Miller-Rabin测试。
但一次MR测试仍然有一定的错误率。为了使我们的结果尽可能的正确,我们需要进行多次MR测试,这样可以把错误率降低。
写成伪代码为:
Miller-Rabin(n): If (n <= 2) Then If (n == 2) Then Return True End If Return False End If If (n mod 2 == 0) Then // n为非2的偶数,直接返回合数 Return False End If // 我们先找到的最小的a^u,再逐步扩大到a^(n-1) u = n - 1; // u表示指数 while (u % 2 == 0) u = u / 2 End While // 提取因子2 For i = 1 .. S // S为设定的测试次数 a = rand_Number(2, n - 1) // 随机获取一个2~n-1的数a x = a^u % n While (u < n) // 依次次检查每一个相邻的 a^u, a^2u, a^4u, ... a^(2^k*u)是否满足二次探测定理 y = x^2 % n If (y == 1 and x != 1 and x != n - 1) // 二次探测定理 // 若y = x^2 ≡ 1(mod n) // 但是 x != 1 且 x != n-1 Return False End If x = y u = u * 2 End While If (x != 1) Then // Fermat测试 Return False End If End For Return True值得一提的是,Miller-Rabin每次测试失误的概率是1/4;进行S次后,失误的概率是4^(-S)。
本题的提示参考了Matrix67的Blog和wikipedia的词条。
Matrix67的Blog有更多的细节描写。Wiki中的伪代码比上文中的简洁一些,并且有介绍了一些小技巧:比如如果n<2^64,只用选取a=2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37做测试即可
AC代码:
//二次探测定理:如果是 P 素数,且0<x<P,则方程x^2=1(mod P)的解为x=1或x=P-1。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <algorithm> #include <iostream> #include <math.h> using namespace std; const int Times = 10; typedef unsigned long long LL; // a * b % n //例如: b = 1011101那么 //a * b mod n = (a * 1000000 mod n + a * 10000 mod n + a * 1000 mod n + a * 100 mod n + a * 1 mod n) mod n LL multi(LL a, LL b, LL m) // 计算 a*b % m { LL ans = 0; a %= m; while(b) { if(b & 1) { ans = (ans + a) % m; b--; } b >>= 1; a = (a + a) % m; } return ans; } LL quick_mod(LL a, LL b, LL m) // 计算 a^b % m { LL ans = 1; a %= m; while(b) { if(b & 1) { ans = multi(ans, a, m); b--; } b >>= 1; a = multi(a, a, m); } return ans; } bool Miller_Rabin(LL n) { if(n == 2) return true; if(n < 2 || !(n & 1)) return false; LL m = n - 1; int k = 0; while((m & 1) == 0) { k++; m >>= 1; } for(int i=0; i<Times; i++) { LL a = rand() % (n - 1) + 1; LL x = quick_mod(a, m, n); //大整数的快速幂算法( (a*a)% n),直接a*a发送数据溢出=>(a%n * a%n)%n LL y = 0; for(int j=0; j<k; j++) { y = multi(x, x, n); if(y == 1 && x != 1 && x != n - 1) return false; x = y; } if(y != 1) return false; } return true; } int main() { int T; scanf("%d",&T); while(T--) { LL n; scanf("%llu",&n); if(Miller_Rabin(n)) puts("Yes"); else puts("No"); } return 0; }
// a * b % n //例如: b = 1011101那么a * b mod n = (a * 1000000 mod n + a * 10000 mod n + a * 1000 mod n + a * 100 mod n + a * 1 mod n) mod n ll mod_mul(ll a, ll b, ll n) { ll res = 0; while(b) { if(b&1) res = (res + a) % n; a = (a + a) % n; b >>= 1; } return res; }
//a^b % n //同理 ll mod_exp(ll a, ll b, ll n) { ll res = 1; while(b) { if(b&1) res = mod_mul(res, a, n); a = mod_mul(a, a, n); b >>= 1; } return res; }
下面开始说Miller-Rabin测试:
费马小定理:对于素数p和任意整数a,有ap ≡ a(mod p)(同余)。反过来,满足ap ≡ a(mod p),p也几乎一定是素数。
伪素数:如果n是一个正整数,如果存在和n互素的正整数a满足 an-1 ≡ 1(mod n),我们说n是基于a的伪素数。如果一个数是伪素数,那么它几乎肯定是素数。
Miller-Rabin测试:不断选取不超过n-1的基b(s次),计算是否每次都有bn-1 ≡ 1(mod n),若每次都成立则n是素数,否则为合数。
伪代码:
Function Miller-Rabin (n : longint) :boolean; begin for i := 1 to s do begin a := random(n - 2) + 2; if mod_exp(a, n-1, n) <> 1 then return false; end; return true; end;
注意,MIller-Rabin测试是概率型的,不是确定型的,不过由于多次运行后出错的概率非常小,所以实际应用还是可行的。(一次Miller-Rabin测试其成功的概率为3/4)
前边说的伪代码实现很简短,下面还有一个定理,能提高Miller测试的效率:
二次探测定理:
如果p是奇素数,则 x2 ≡ 1(mod p)的解为 x = 1 || x = p - 1(mod p);
可以利用二次探测定理在实现Miller-Rabin上添加一些细节,具体实现如下:
bool miller_rabin(ll n) { if(n == 2 || n == 3 || n == 5 || n == 7 || n == 11) return true; if(n == 1 || !(n%2) || !(n%3) || !(n%5) || !(n%7) || !(n%11)) return false; ll x, pre, u; int i, j, k = 0; u = n - 1; //要求x^u % n while(!(u&1)) { //如果u为偶数则u右移,用k记录移位数 k++; u >>= 1; } srand((ll)time(0)); for(i = 0; i < S; ++i) { //进行S次测试 x = rand()%(n-1) + 1; //在[2, n)中取随机数 if((n%x) == 0) continue; x = mod_exp(x, u, n); //先计算(x^u) % n, pre = x; for(j = 0; j < k; ++j) { //把移位减掉的量补上,并在这地方加上二次探测 x = mod_mul(x, x, n); //二次探测定理,这里如果x = 1则pre 必须等于 1,或则 n-1否则可以判断不是素数 if(x == 1 && pre != 1 && pre != n-1) return false; pre = x; } if(x != 1) return false; //费马小定理 } return true; }一、 先介绍几个定理
费马小定理,二次探测定理
著名的费马小定理为素数判定提供了一个有力的工具.
费马小定理:如果p是一个素数,且(0<a<p),则
证明是容易的.
如果p是素数,x是小于p的正整数,且,那么要么x=1,要么x=p-1。
这是显然的,因为相当于p能整除,也即p能整除(x+1)(x-1)。
由于p是素数,那么只可能是x-1能被p整除(此时x=1) 或 x+1能被p整除(此时x=p-1)。
由费马小定理我们知道,如果p是素数,那么a的p-1次方mod p =1.这个定理成立,那么他的逆否命题肯定成立,也就是凡是a^(p-1)Mod p != 1的,那么p一定是合数,就是不是素数,但是这有一个条件,那就是a和p得互质。
逆否命题成立了,但是人们更关心的是逆命题,因为这样可以判断一个数是否是素数,如果逆命题成立,那么就可以通过a^(p-1)Mod p = 1来判断p是素数了。但是想象是美好的,现实是残酷的。
1819年有人发现了Fermat小定理逆命题的第一个反例:虽然2的340次方除以341余1,但341=11*31,却不是素数。后来,人们又发现了561, 645, 1105等数都表明a=2时Fermat小定理的逆命题不成立。人们把所有能整除2^(n-1)-1的合数n叫做伪素数(pseudoprime)。
不满足的n一定不是素数;如果满足的话则多半是素数。这样,一个比试除法效率更高的素性判断方法出现了:制作一张伪素数表,记录某个范围内的所有伪素数,那么所有满足且不在伪素数表中的n就是素数。之所以这种方法更快,是因为我们可以使用二分法快速计算的值(快速幂)。
然而不借助伪素数表的时候,算法出错的概率太高,需要改进.
我们刚才只考虑了a=2的情况。一个合数可能在a=2时通过了测试,但a=3时的计算结果却排除了素数的可能。于是,人们扩展了伪素数的定义,称满足a^(n-1) mod n = 1的合数n叫做以a为底的伪素数(pseudoprime to base a)。
随机选择若干个小于待测数的正整数作为底数a进行若干次测试,只要有一次没有通过测试就立即把这个数扔回合数的世界。这就是Fermat素性测试。
费马小定理毕竟只是素数判定的一个必要条件.满足费马小定理条件的整数n未必全是素数.有些合数也满足费马小定理的条件.这些合数被称作Carmichael数,前3个Carmichael数是561,1105,1729. Carmichael数是非常少的.在1~100000000范围内的整数中,只有255个Carmichael数.
费马小定理的前提是a和n互质。当n本身就是素数的时候如果a<n那么a和n始终互素;但n不是素数时a和n不互素的话不能用费马小定理。也就是说,Carmichael数需要排除a和n不互素的情况.
二次探测:
如果p是素数,x是小于p的正整数,且,那么要么x=1,要么x=p-1。
这是显然的,因为相当于p能整除,也即p能整除(x+1)(x-1)。
由于p是素数,那么只可能是x-1能被p整除(此时x=1) 或 x+1能被p整除(此时x=p-1)。
我们下面来演示一下上面的定理如何应用在Fermat素性测试上。前面说过341可以通过以2为底的Fermat测试,因为2^340 mod 341=1。如果341真是素数的话,那么根据二次探测定理有2^170mod 341只可能是1或340;当算得2^170 mod 341确实等于1时,我们可以继续查看2^85除以341的结果。我们发现,2^85 mod 341=32,这一结果摘掉了341头上的素数皇冠
这就是Miller-Rabin素性测试的方法。不断地提取指数n-1中的因子2,把n-1表示成(其中d是一个奇数)。那么我们需要计算的东西就变成了除以n的余数。于是,要么等于1,要么等于n-1。如果等于1,定理继续适用于,这样不断开方开下去,直到对于某个i满足或者最后指数中的2用完了得到的。这样,Fermat小定理加强为如下形式:
尽可能提取因子2,把n-1表示成,如果n是一个素数,那么或者,或者存在某个i使得 ( 0<=i<r ) (注意i可以等于0,这就把的情况统一到后面去了)
问题已经很清楚了,只需要判断所有的二次探测满足条件就可以判定一个数极有可能是素数(不能肯定,因为逆命题不成立,前面已经说明过了)。那算法如何实现呢?
根据二次探测,我们将n-1化简为d*2^r,从a^d次方算起mod n是否是1或者n-1,如果不是,直接判断不是素数。如果是,那么判断a^(d*2)也就是a^(d*2^1),刚好符合二次探测定理。也和上面2^340,2^170,2^85。。。一样,只不过是从小向大计算。这样通过了所有以2为底的,可能是素数,可能性不是很高,如果能通过以3为底,以5为底。。。次数越多,得到是素数的概率越高。一般随机10个a,就能达到99.99%的正确率。