首页 > Python算法 > 数论算法 阅读数:23

素性检验算法

你能很迅速地找出两个比 1000 大的素数吗?你能很迅速地分辨 754729 是哪两个素数的乘积吗?实际上,因为素数相对密集,所以我们可以相对迅速地找出两个大的素数。但是,现在还没有已知算法能够有效地分解乘机中的因子。因此,在现代密码学中,利用这个时间差距,计算机科学家们发明了安全的加密系统,如 RSA 与 Rabin。

简单来说,加密信息的人需要随机地找出两个素数并且公开它们的乘积,如果他人想要破解信息的话必须分解被公开的乘积。这样,只要加密的人找到两个足够大的素数,第三方就没有方法破解信息。

但是,加密信息的人寻找素数时怎么才能知道一个随机的很大的数字到底是不是一个素数呢?他当然可以挨个数字尝试做除法检查,但是那样太耗时,毕竟他随机找出的是一个几百位长的数字。我们需要一个更有效率的算法。

1. 费马素性检验

我们接下来了解费马素性检验,虽然这个算法在现实中不常用,但是却构成了绝大多数素性检验算法的底层逻辑。

首先,我们需要了解费马小定理(Fermat’s Little Theorem)。

定理 1:

假如 a 是一个整数,p 是一个素数,且 a 不是 p 的倍数,那么 ap−1≡1(mod p)

比如,7 是一个素数,对任何一个不是 7 的倍数的整数 a 来说 a6≡1(mod 7)。

费马素性检验算法如下:
  1. 已知正整数 p,在 [2,p−1] 的范围内随机选择 a ;
  2. 计算 ap−1(mod p);
  3. 如果结果不等于 1,输出不是素数(是合数);
  4. 如果结果等于 1,输出可能是素数。

费马素性检验实际上就是费马小定理本身,如果一个数不满足费马小定理,则判断该数为合数;如果一个数满足费马小定理,则判断该数可能为素数。

费马小定理能够完全准确地判断一个数是不是素数,但它只能几乎完全准确的判断一个数是不是素数。比如,6 不是素数因为 25 ≡1(mod 6),9 不是素数因为 28 ≡1(mod 9),我们可以十分有把握地利用费马小定理来断定一个不定素数。

之前很长一段时间,学者们都猜测费马定理的逆命题同样成立,直到人们发现了伪素数的存在。伪素数是满足费马小定理的合数。比如,2340 ≡1(mod 341),但是 341 并不是一个素数。我们称 341 为关于 2 的费马伪素数。

伪素数出现率非常低,比 2.5×1010 小的关于 2 的伪素数只有 21853 个,也就是说出现率为 0.0000009%。关于 2 的伪素数的意思是当 a 为 2 时,数字满足定理。换一个 a,这个伪素数就不一定满足定理了。但是有一些伪素数能够满足一个或更多的 a。比如 341 同时满足 a=2 和 a=3。

有一种更特殊的伪素数,我们称他们为卡迈克尔数(Carmichael Number)。即无论 a 是什么数,它都满足费马小定理。比如,561 是最小的卡迈克尔数,它满足 a560 ≡1(mod 561),不论 a 是 2,3,4,…,只要不是 561 的倍数费马小定理都成立,然而 561=3×11×17。可想而知,卡迈克尔数的出现率更低,比 2.5×1010 小的卡迈克尔数只有 2163 个。然而,虽然出现率低,世界上却有无穷个卡迈克尔数。

因为伪素数的存在,所以费马素性检验不完全可靠。但是,如果我们以不同的 a 为底重复测试,便可降低错误率。当我们随机选择 a 时,我们可以肯定“n 不是以 a 为底的伪素数”的概率至少是 50%。如果我们只做一次检验,那么成功率至少为 50%。如果做两次检验,成功率至少为 75%。如果我们做十次检验,成功率至少为 99.9%。这个成功率已经非常令人满意了。

然而,重复检验并不能避开狡猾的卡迈克尔数。而且世界上有无穷个卡迈克尔数,所以有时我们需要一个更加完整的素性测试。

2. 米勒-拉宾素性检验

米勒-拉宾素性检验(Miller-Rabin Test)首先是由米勒(Gary Miller)在 1976 年提出的。后来在 1980 年,拉宾(Michael Rabin)对米勒的算法作了修改,所以检验以两者姓氏命名。

米勒-拉宾素性检验是目前最被认可的素性检验。它在费马素性检验的基础上又添加了另外的判断条件,令卡迈克尔数更难“骗”住它。但是,它和费马素性检验一样只能判断一个数字是合数还是可能是素数。

在分析米勒-拉宾素性检验之前,我们先了解一个数学知识。

定理 2:

当 p 为大于 2 的素数时,x2 ≡1(mod p) 的解为 x≡±1(mod p)。

比如,因为 5 是一个大于 2 的素数,所以关于任何 x,x2 ≡1(mod 5) 的解都为 x≡±1(mod 5)。与费马定义一样,是素数的数一定会满足这个条件,但满足条件的不一定全是素数。

证明定理 2:

x2 ≡1(modp) 可以被表达为 x2−1≡0(modp),即 x2−1 能够被 p 整除。而 x2−1≡(x+1)(x−1),因此 (x+1) 或 (x−1) 肯定能够被 p 整除,即 x≡±1(mod p)。 让我们利用定理 2 测试一下 561。上节提到 561 是一个卡迈克尔数,虽然不是素数,但它仍然能通过以任何数为底的费马检验,比如以 2 为底时,2560 ≡1(mod 561)。

根据定理 2,如果 561 是一个素数的话,那么 2280(mod 561) 只能等于 1 或 560。因为 2560 相当于 (2280)2,想象 x2 等于 2560,那么,x 就等于 ±2280。如果 2560 ≡1(mod 561),那么 2280(mod 561) 只能等于 1 或 560。在这里,我们用 x=560(mod 561) 代替了 x=1(mod 561)。

事实上 2280(mod 561) 很幸运,真的等于 1。那我们继续,如果 561 是一个素数的话,2140(mod 561) 只能等于 1 或 560。这一次,想象 x2 等于 2280,x 等于 ±2140。如果 2280 ≡1(mod 561),那么 2140(mod 561) 只能等于 1 或 560。

这次 561 没有那么幸运,2140(mod 561) 等于 536。因此我们成功判断 561 为合数。而另外两个例子,97 和 89,是素数并且被判断为可能为素数,如图 1 所示。
利用定理 2 测试561、97、89是否为素数
图 :1:利用定理 1 测试 561、97、89 是否为素数

通过定理 2,我们可以得出以下结论。

令 n 为被检验的数字,表达 n-1 为 2kd,d 为奇数。比如 56 等于 23×7。如果 n 是一个素数,那么只有以下两种可能:
  • ad≡1(mod n) 或 ad≡n−1(mod n)。
  • a2id≡n−1(mod n),0<i<k。

第一种可能代表我们一直开方并且每一次的结果都是 1,直到 2kd 变成 ad,例如图 1 中的 89。第二种可能代表我们半途得到 -1,所以不能再继续开方,例如图 1 中的 97。另外,米勒-拉宾检验用不同的底数进行多次测试,从而避免 n“很幸运地”是关于当前底数的伪素数的情况。

米勒-拉宾素性检验算法如下:

给定 n,m。n 为需要检验的数字,m 为测试重复次数,m 越大检验的精确性越高。

1) 表达 n-1 为 2kd。

2) 重复 m 次以下步骤:
① 随机在 [2,n-2] 的范围内选择底数a。
② 如果 ad(mod n)=1 或 n−1,跳过本次循环。
③ 从 i=1 开始,计算 a2id(mod n)。
④ 如果 a2id(mod n) 等于 n−1,跳过本次循环。
⑤ 返回 c,i=i+1,直到 i=k−1。
⑥ 判断 n 不是素数。

3) 判断 n 可能是素数。
第(2)步中的 ② 步检查了 ad≡1(mod n) 或 ad≡n−1(mod n) 的可能,第(2)步中的 ④ 步检查了 a2id≡n−1(mod n),0<i<k 的可能。如果 n 没有满足任何一个条件,那么它肯定是合数。但是,即便 n 满足了其中一个条件,它还需要被不同的 a 继续检查,毕竟它可能是一个以当前 a 为底的伪素数。

概括地讲,米勒-拉宾素性检验做排除法,只在肯定的情况下否定一个数是素数;如果一直到循环尽头数字还没有被否定,就判断它可能是素数。

3. 算法代码

我们定义两个方法,checkComposite() 和 miller_rabin()。miller_rabin() 调用 m 次 checkComposite() 方法,每一次传入不同的底数。如果数字是合数,checkComposite() 返回 True。
from random import randrange  #引入randrange
#以a为底数,检查n是否为合数,n-1 = (2^k)d
defcheckComposite(a,k,d,n):
    x = pow(a, d, n)           #pow(a,d,n)代表a^d(modn)
    if x == 1 or x == n - 1:       #满足条件1
        return False
    for _ in range(k-1):
        x = pow(x, 2, n)
        if x == n - 1:           #满足条件2
            return False
    return True             #两个条件都不满足,n是合数
#n是被检验的数,m是精准度
#判断n是否不是合数
defmiller_rabin(n, m):
    if n == 2 or n == 3:         #2和3需要独立考虑
        return True
    if n % 2 == 0:            #偶数情况
        return False
    k = 0
    d = n-1
    while d % 2 == 0:          #计算k与d
        k += 1
        d = d//2
    for _ in range(m):          #调用checkComposite方法m次
        a = randrange(2, n - 1)      #随机选择底数
        if (checkComposite(a,k,d,n)):
            return False
    return True             #n可能是素数