Pollard Rho算法浅谈
Pollard Rho介绍
Pollard Rho算法是Pollard[1]在1975年[2]发明的一种将大整数因数分解的算法
其中Pollard来源于发明者Pollard的姓,Rho则来自内部伪随机算法固有的循环
Pollard Rho算法在其他因数分解算法[3]中不算太出众,但其空间复杂度Θ(1)的优势和好打的代码使得OIer更倾向于使用Pollard Rho算法
毕竟试除法太慢了,谁没事打Pollard Rho不打试除法
Pollard Rho原理
生日悖论
如果一年只有365天(不计算闰年),且每个人的生日是任意一天的概率均相等,那么只要随机选取23人,就有50%以上的概率有两人同一天生日
解释:第一个人不会和前面的人重生日(因为前面没有人),第二个人不重生日的概率为364/365,第三个人363/365……以此类推,那么只要到第23个人,就有,说明这时就有50%以上的概率有两人同生日
更多的,当一年有n天时,只要人数到达Θ(sqrt(n))的数量级,有至少两个人同一天生日的概率就可以达到50%以上
图象表达:
Miller Rabin素性测试[4][4']
其实Miller Rabin素性测试不是那么重要(因为不是Pollard Rho算法的核心),但没有它,Pollard Rho算法会被试除法拖慢到比朴素算法更差
这里就不再赘述,想要学习的请移步至维基百科(百度百科真是垃圾)
快速幂&比硬件还慢的快速乘&GCD
这应该是大多数数论算法的标配,不会的请先百度或谷歌
Floyd判圈算法[5]
Floyd判圈算法又叫龟兔赛跑算法,是用差速转移的方式快速判断是否进入环的算法
想象有一只兔子和一只乌龟按一个确定函数从同一个起点移动,兔子每次可以转移两次,乌龟可以转移一次
(OI算法中一张奇怪的图片)
那么如果这个确定函数上没有环,那么兔子一定会在乌龟前面,二者一定不会相交
否则,由于函数上有环,兔子一定会“追上”乌龟,也就是“套圈”
不仅如此,对算法分析可知,当兔子“追上”乌龟时,兔子也一定跑了刚好一圈(就是不证明,你来打我呀)
Pollard Rho算法的做法
介绍所有算法前先要声明一下:在每次操作前要先判断是不是素数,否则找不出原数的非平凡因子
这里要用Miller Rabin素性测试快速判断,以保证其高效性(不然提到这家伙是干什么的)
GCD优化+随机化试除法
试除法是一种朴素的因数分解算法,加了随机化后其实并没有多大用而且还更慢了
但是如果我们退而求其次——不直接寻找因子,而是寻找因子的倍数,然后用GCD找到因子本身呢
随机寻找到一个因子的倍数的概率为,确实比直接找因子的随机化试除法好了一点点,但当因子很大时依旧没什么用
PS:在Pollard Rho算法中随机数算法一般用F(x)=x2+c(这个函数具有混沌性,实现又简单,而且调整参数c可以保证所有的数一定都能遇到,所以一般用这个算法,而不用其他算法)
向成熟的Pollard Rho算法迈进
把原来的一个随机数变成两个数的差,这样就得到了比较常见的Pollard Rho算法模板
看上去似乎并没有什么用(实际上对效率也没什么帮助),但是我们可以把两个随机数设为Floyd判圈算法中的“兔子”和“乌龟”,从而在走上“不归路”前及时退出
PS:看上去似乎到现在都还没有用到生日悖论的概率增大,但是实际上当寻找次数比较小时不记录的随机和记录的随机概率相近
减少GCD数目——过河拆桥优化法
每次产生一对随机数都要求一次GCD,对算法的效率有着很大影响。有什么办法可以减少求GCD的次数呢?
我们可以发现,把多个数相乘再取GCD一定不会使找出的因子大小更小
所以说,我们可以将K个差相乘,然后GCD一次判断,找到非平凡因子的概率就更大了
需要注意的是,当乘积变成要求分解的数的倍数时,说明最后一对随机数一定对结果有贡献,数对差一定是一个因子的倍数
不仅如此,当循环因为找到环而退出时应该把已有的乘积进行判断(连尸体都不放过)
PS:这里的K一般取127,我也不知道为什么
《算法导论》的优化——倍增求圈法
当随机数的转移算法比较复杂时,Floyd判圈算法的多次转移就会成为性能的瓶颈。有什么办法可以少转移呢?
可以想到,我们可以不直接寻找环的出现,而是在转移时标记一个记录点,当发现走到了记录点,就说明我们已经走了至少一圈环
但又要怎么打标记点呢?打得太少会退化,打得太多又会影响效率,必须要和环的长度差不多才行,这就变成了猜测环的长度的问题了
而每次猜测环的长度可以用倍增的方法,首先猜环的长度在K以内,如果没走到,那么不是猜小了就是记录点选错了,把记录点换成现在这个点,再继续猜测环的长度为2K
所以又怎么替代Floyd判圈算法呢?由于记录点自身也是随机数,具有一定的随机性,那就干脆直接代替Floyd判圈算法的“乌龟”得了
在已有算法上卡一卡常——让子弹再飞一会
- Miller Rabin素性测试的正确性很高,按洛谷P4718的题解中_皎月半洒花所说(现在原文章已改,无此段),在测试时只要测2和61就能过,但是要能100%过,需要从2到23的所有素数都试一遍[6]
- 用register运算符修饰临时变量,优化硬件速度(register可以让数据被存储到更快的CPU寄存器中)
- 用快一点的快速乘算法,并尽量减少强制类型转换的次数
P4718的代码——Pollard Rho算法
1 #define LL long long 2 bool IsPrime(LL);//返回素性测试结果 3 LL GCD(LL,LL);//返回两个数的GCD 4 LL Mix(LL,LL,LL);//返回两个数在模运算下的乘积 5 6 void MaxFactor(LL n,LL &ans){ 7 if(n==1||n<=ans||IsPrime(n)){ 8 ans=max(ans,n); 9 return;//判断特殊情况:n为1或素数,n一定不能更新ans 10 } 11 for(LL c=rand()%(n-1)+1;;c++){ 12 //为防止随机数无效化,使用死循环 13 LL t1=rand()%(n-1)+1,t2=(Mix(t1,t1,n)+c)%n; 14 LL p=1,i=0,g=0; 15 while(t1!=t2){ 16 p=Mix(p,abs(t1-t2),n); 17 if(!p){//乘积为0时说明找到了一个因子 18 g=GCD(n,abs(t1-t2)); 19 if(g>1&&g<n){ 20 MaxFactor(g,ans); 21 MaxFactor(n/g,ans); 22 } 23 return; 24 } 25 ++i; 26 if(i==127){//当有127个数时强制测试 27 g=GCD(n,p); 28 if(g>1&&g<n){ 29 MaxFactor(g,ans); 30 MaxFactor(n/g,ans); 31 return; 32 } 33 p=1,i=0; 34 } 35 t1=(Mix(t1,t1,n)+c)%n; 36 t2=(Mix(t2,t2,n)+c)%n; 37 t2=(Mix(t2,t2,n)+c)%n; 38 } 39 g=GCD(n,p); 40 if(g>1&&g<n){//循环退出后收尾 41 MaxFactor(g,ans); 42 MaxFactor(n/g,ans); 43 return; 44 } 45 } 46 }
参考资料
洛谷日报:https://www.luogu.org/blog/zenyz/pollardrho-kuai-su-fen-xie-zhi-yin-shuo
P4718题解:https://www.luogu.org/problemnew/solution/P4718
Pollard Rho算法——维基百科:https://en.wikipedia.org/wiki/Pollard%27s_rho_algorithm
Miller Rabin素性测试——维基百科:https://zh.wikipedia.org/wiki/%E7%B1%B3%E5%8B%92-%E6%8B%89%E5%AE%BE%E6%A3%80%E9%AA%8C
——会某人