0.1 一些闲话
最近一次更新是在2019年11月12日。之前的文章有很多问题:当我把我的代码交到LOJ上,发现只有60多分。我调了一个晚上,尝试用{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 61, 24251, 2147483647, 998244353}
这么一大串数作为基底,然后左改右改,总算过去了。特别感谢 @骗分过样例 的提醒,现在张贴的代码应该是值得信赖的了。
之前我的同学好像就指出过我的文章的很多问题。比如说我之前写到,Miller Rabin在面对合数\(46856248255981\)时,如果用{2,3,7,61,24251}
做基底,是会判断错误的。但实际上,他说他对着我的代码写了一遍,发现这个数是可以判掉的。
OI中的数学需要细心。相比其他算法方面,数学真的不好调试——一个公式算错了,一个下标写反了,程序就错了。而且复杂的数学代码很难用gdb查错,只能反复自己的公式是否写对,并且在转换成代码的时候是否有差错。编程需要细致和求实精神。当你在写代码,亦或是在写题解时,多问自己一个:有没有问题?是不是哪里写错了?这里为什么要这么写?可不可以造数据Hack?尤其是在写博客的时刻,每一个OIer都需要做到足够细致——因为这些文章不是写出来好看,让同学膜拜的,而是真的要帮到网络另一端,需要帮助的人。
这是我尤为欠缺的。最近我的博文被指出了很多错误,我也意识到自己的不细致会给自己带来这么多麻烦。也希望我能作为反面教材,警醒后人吧。
1.1 问题引入
判断一个数\(n\)是不是质数.
当n不太大时,我们直接用\(O(\sqrt{n})\)的试除法就可以了.听说还可以利用素数定理,只用\(O(ln(x))\)就可以了?这种说法我还暂时没试过.
不过,对于\(n \geqslant 10^{18}\)时,这个算法是肯定不可取的.我们需要一个更加快速的算法.谁不希望所有的问题被\(O(1)\)解决呢?
一想到\(O(1)\),我们不妨试一下随机算法.在之后介绍Pollard Rho算法后,也会用到这种思想.
1.2 随机的依据
按理来讲,我们应该是在\([1,\sqrt{n}]\)里面随机,再通过某些方式来提高正确率的.但是这样做太慢!我们要确保每一个在\([1,\sqrt{n}]\)中的数都不是n的因子,显然用随机试的算法是行不通的.那么,我们该怎么办?
数论中,我们有Fermat(也就是费马)小定理:
$ p \in \text{Prime},\quad x^p \equiv p \pmod{p}$ 或 $ x^{p-1} \equiv 1 \pmod{p}$.
这个定理对于任何质数成立;但是反过来不一定.满足Fermat小定理的数不一定是质数.事实上,如果对任意$ b \in \mathbb{N}, b^{p-1} \equiv 1\pmod{p}$,我们就称p为Carmichael数.运用Fermat小定理,我们可以判定一个数不是质数,但是不能判定一个数是质数.
不过,这给我们的随机枚举提供了一个不错的启示:不是随机枚举\(n\)的因子,而是一个基底\(b\).若对于不同的基底,都有\(b^{p-1} \equiv 1 \pmod{p}\),p是质数的可能性就更大了.事实上,如果p满足\(b^{p-1} \equiv 1 \pmod{p}\),那么p就叫做"基于b的伪素数".
如果可能的话,这里会张贴费马小定理的证明.
1.3 算法的改进: 二次探测定理
这个定理叫做"二次"是有原因的.其一,它是费马小定理探测质数的辅助工具,作为第二道屏障;其二,它和一个"二次同余方程"有关.定理如下:
若\(x^2 \equiv 1 \pmod{p}\),则 \(x \equiv 1 \pmod{p}\) 或 \(x \equiv p-1 \pmod{p}\)
这个定理比较好证明.这里是证明过程.
对于一个质数p,只要p和二次探测定理不符,那么我们就可以肯定p不是质数.事实上,我们常常直接用这个定理去判定质数,而不是用费马小定理.
2.1 基本实现
无论是费马小定理还是二次探测定理,我们都需要选取若干个合适的基底来进行测试.一般而言,我们会选取这五个基底:\(2,3,7,61,24251\)。当需要测试的数据规模在\(10^{16}\)左右时,它的表现效果还是可以的。但当数据规模更大些时,你必须考虑将基底扩大一些。在这个基底的基础上,可以考虑选取前\(10\sim 15\)大的质数,从而解决几乎\(100\%\)的数据。
对于每一个基底\(b\)和一个待判断的数\(x\),我们进行如下测试:
1.用费马小定理判定一下这个数是不是合数.
2.如果\(x-1\)是偶数,我们可以对费马小定理做如下变形:
\(b^{x-1}\equiv 1 \pmod{x} \implies b^{\frac{x-1}{2}\cdot 2} \equiv 1 \pmod{x}\)
这样就可以用二次探测定理看一下有没有\(b^{\frac{x-1}{2}} \equiv 1 \text{或} x-1 \pmod{x}\) 了.如果都不满足,x就一定不是质数.
如果\(\frac{x-1}{2}\)同样是一个偶数,我们可以继续将它拆成\(\frac{x-1}{4} \cdot 2\),然后再用一次二次探测定理来做.
3.对于二次探测定理\(X^k \equiv 1 \pmod{x}\),如果\(k\)是奇数或者\(X \equiv x-1 \pmod{x}\),此时我们没有办法再拿这个数做文章了.我们只能暂时认定\(x\)是质数.
注意到这就是Miller Rabin算法具有随机性的证据之二:对质数的判定不充分.但事实上,用上面这种方法已经可以成功地判定许多质数了,在实际应用中是值得信赖的.
上面这个算法的时间复杂度是\(O((\log n)^2)\)的:瓶颈在于快速幂,和分解待验证的数;但实际运行时,速度是相当快的。
(之前这里是有代码的,但是好像有点bug。这里就不张贴了。)
2.2 算法的优化
显然,我们不难看出上述代码有大量需要优化的地方.就比如说,开始的那次费马小定理的判定是完全没有必要的.我们可以直接把这一过程留到二次探测定理去执行.
其次,我们使用二次探测定理时每次都要探测\(\log n\)次,这个次数是可以稍微有所优化的.
对于前者,我们直接删去开头的费马小定理判断就可以了.对于后者,网上广为流传这样一种优化技巧:
设\(k=x-1=2^p \cdot d\),\(d\)是一奇数,那么之前二次探测定理\(X^2 \equiv 1 \pmod{x}\)检测的数\(X\)分别是如下几个数:\(b^{2^p*d},b^{2^{p-1}*d},b^{2^{p-2}*d},\dots,b^{d}\).
如果我们从后往前检测,如果其中一个数\(X\)通过了二次探测定理,就直接判定\(x\)是质数.
我会尝试着证明这个的.日后可能会贴在这里.
这个新算法的流程如下:
1.按上面的方法计算出d和p.记\(cur=b^d\).如果\(cur\equiv 1 \text{或} x-1 \pmod{x}\)就直接判定x是质数.
2.每次把\(cur\)赋值为\(cur^2 \pmod{x}\)直到\(cur=b^k\).这个过程等价与将\(cur\)从\(b^d\)往\(b^k\)的方向扫描.
3.如果\(cur = x-1\)则直接判定x为质数.否则转2.
4.如果上述测试都没有判定x为质数,则直接判定x为合数.
bool mr(ll x,ll b)
{
ll d,p=0;
d=x-1;
while((d&1)==0)
d>>=1,++p;
int i;
ll cur=qp(b,d,x);//快速幂
if(cur==1 || cur==x-1)
return true;
for(i=1;i<=p;++i)
{
cur=cur*cur%x; //为了防止溢出,这里最好用快速乘或者直接用__int128转化一下
if(cur==x-1)
return true;
}
if(i>p)
return false;
}
bool mr(ll x)
{
if(x==46856248255981 || x<2)
return false;
if(x==2 || x==3 || x==7 || x==61 || x==24251)
return true;
return mr(x,2)&&mr(x,3)&&mr(x,7)&&mr(x,61)&&mr(x,24251);
}
算法的具体实现我也是参考了洛谷P4718大佬@Great_Influence 的题解,不太清楚背后的原因.可以参考一下他的代码.
来源:oschina
链接:https://my.oschina.net/u/4390329/blog/4289008