[学习笔记] Miller-Rabin质数测试 & Pollard-Rho质因数分解
Miller-Rabin质数测试 & Pollard-Rho质因数分解
考试遇见卡质因数分解的题了...活久见...毒瘤lun
于是就学了一发qaq
Pollard-Rho分解质因数的话需要依赖另一个算法.
Miller-Rabin质数测试
一个多项式时间的基于随机的质数测试算法.
玄学.
一些依赖的定理
费马小定理
若 \(p \in \mathbb P\) 则 \(\forall a\in \mathbb N^+, a^{p-1} \equiv 1\pmod p\)
它的逆命题基本上是真的, 于是我们可以把这个定理的逆命题作为判断质数的依据.
为啥说"基本上"是真的呢? 因为有些鬼畜的合数 \(n\) 满足 \(\forall a\in \mathbb N^+,a^{n-1}\equiv 1 \pmod n\). 这种鬼畜的合数被称为 Carmichael 数. 前三个 Carmichael 数是 \(561\), \(1105\) 和 \(1729\). OEIS数列编号 A002997 注意到它们可以挺小的...所以只用这个定理的逆命题测试有很大的问题.
二次探测引理
这个名字好像不是很通用...但是我们在这里先这么叫好了...
若 $p\in \mathbb P $ , 则 \(1\bmod p\) 不存在非平凡二次剩余.
我们尝试用它来作为判质数的条件, 那么就可以:
若 \(\forall a^2 \equiv 1 \pmod p, a\equiv \pm1 \pmod p\), 则 \(p \in \mathbb P\)
这个命题也基本上是真的...
但是也有些强伪质数满足上面这个条件...
不过没有合数能同时通过上面的两个测试.
具体用法是先令 \(n-1=2^tu\), 其中 \(u\) 是奇数. 然后随机一个值 \(a\), 把 \(a^u\) 平方 \(t\) 次. 如果某次平方出 \(1\) 了, 那么判断一下平方前的值是否是 \(\pm 1\). 如果是那么就算通过了二次探测. 通过二次探测后刚好求出了 \(a^{2^tu}\) 也就是 \(a^{n-1}\), 判一下是不是 \(1\) 就可以验证费马测试. 如果都是, 那么就称 \(n\) 通过了 \(a\) 的测试, 或者说 "\(a\) 不是 \(n\) 是合数的证据".
实现以及正确率
具体过程大概是
- 把乱七八糟的trival情况判掉, 比如 \(\le 2\) 啊, 偶数啊啥的.
- 进行 \(s\) 轮测试, 每次随机一个底数 \(a\) 进行上文所述的合并测试.
正确率大概要靠下面这个结论:
若 \(n\) 是一个奇合数, 那么 \(n\) 为合数的证据数量至少为 \(\frac{n-1}2\).
具体证明不详细展开了...算法导论上证了...
那么也就是说, 对于一个合数我们随机选择一个 \(a\) 然后刚好命中不是证据的 \(a\) 的概率小于 \(\frac 1 2\). 进行 \(s\) 轮测试后均命中非证据的概率小于 $2^{-s} $.
实际上不是证据的数的数量远远达不到 \(\frac {n-1} 2\). 能证明的最好结论是非证据的 \(a\) 最多有 \(\frac {n-1} 4\) 个. 而且这个界是能达到的.
所以实际正确率大概是 \(1-4^{-s}\). 一般取 \(s=10\) 效果就已经很好了.
一般用的时候要快速乘. 龟速乘(倍增加法)多半会GG.
代码实现
下面是板子题 LOJ #143. 质数判定 的AC代码
#include <bits/stdc++.h>
typedef long long intEx;
bool MillerRabin(intEx);
inline intEx RandInt(intEx,intEx);
inline intEx Mul(intEx,intEx,intEx);
inline intEx Pow(intEx,intEx,intEx);
int main(){
for(intEx x;scanf("%lld",&x)!=EOF;){
if(MillerRabin(x))
puts("Y");
else
puts("N");
}
return 0;
}
bool MillerRabin(intEx n){
static const int ROUND=10;
if(n<2)
return false;
if(n==2)
return true;
if(!(n&1))
return false;
int p=0;
intEx m=n-1;
while(!(m&1))
++p,m>>=1;
for(int k=0;k<ROUND;k++){
intEx pw=Pow(RandInt(1,n-1),m,n);
intEx last=pw;
for(int i=0;i<p;i++){
pw=Mul(pw,pw,n);
if(pw==1&&last!=1&&last!=n-1)
return false;
last=pw;
}
if(pw!=1)
return false;
}
return true;
}
inline intEx Mul(intEx a,intEx b,intEx p){
intEx t=a*b-(intEx)((long double)a*b/p+0.5)*p;
return t<0?t+p:t;
}
inline intEx RandInt(intEx l,intEx r){
static std::mt19937_64 mt(int(new int));
return std::uniform_int_distribution<intEx>(l,r)(mt);
}
inline intEx Pow(intEx a,intEx n,intEx p){
intEx ans=1;
while(n>0){
if(n&1)
ans=Mul(ans,a,p);
a=Mul(a,a,p);
n>>=1;
}
return ans;
}
Pollard-Rho质因数分解
这个算法也是个概率算法. 不过它运行时间是期望的...
首先它也有个依赖的结论
生日悖论与生日攻击
首先是生日悖论. 一个 \(23\) 人的团体存在两人生日相同的概率要大于 \(50\%\). 一个推论是在 \(n\) 个值中随机选取若干个值, \(O(\sqrt n)\) 次后就会有很大概率产生某个值与之前选的值重复的情况.
大概证明可以长这样:
我们补集转化一下, 转而求选出的 \(k\) 个值都不同的概率. 显然它应该长这样:
我们只要让这个值小于 \(\frac 1 2\) 就好了. 而由泰勒展开可得:
那么对于 \(x> 0\) 有:
于是就有:
那么我们只要让不等式右边小于 \(\frac 1 2\) 就好了. 那么我们有:
两边取对数解一下就有:
又因为 \(\ln 2\) 是个常数, 于是 \(k=O(\sqrt n)\).
生日攻击是一种现代密码学攻击方法, 就是利用上面的生日悖论来制造Hash碰撞. 主要攻击对象为数字签名. 如果值域为 \(U\) 的话期望生成 \(\sqrt U\) 种不同信息即可发生碰撞. 如果产生Hash碰撞则可以进行伪造数字签名等等一系列攻击行为. Cookie也需要防范这种情况.
主要思想
设我们要对 \(n\) 进行因数分解, \(n\) 存在一个非平凡因子 \(p\) 且 \(p\le n/p\).
我们取一个次数至少为 \(2\) 且包含常数项的多项式函数 \(f(x)\) (比如 \(f(x)=x^2+c\), \(c\) 随机取), 设 \(g(x)=f(x)\bmod n\), 用 \(g\) 来生成伪随机数. 方法是随机选取一个初始值 \(r\) 不断将新的 \(x\) 代入 \(g(x)\) , 也就是 \(x_1=g(r), x_2=g(g(r)),x_3=g(g(g(r)))\) . 那么我们可以认为数列 \(\langle x_k\rangle\) 看上去是随机的. 而且 \(x_k=g(x_{k-1})\).
也就是说这个伪随机数列只和它的上一个输出有关. 从一个相同的初值可以得到相同的序列.
由于 \(p\mid n\), 所以数列 \(\langle x_k \bmod p\rangle\) 也满足这个性质, 即 \(x_k\equiv g(x_{k-1}) \pmod p\). 因为这个伪随机数生成器的状态只和上一个输出有关, 那么只要生成的新值命中了之前已经生成的值, 那么这个随机数列就会出现环.
由于 \(p\) 比较小, \(\langle x_k \bmod p \rangle\) 有很大概率比 \(\langle x_k \rangle\) 更早出现环 (只要新生成的 \(x\) 对 \(p\) 取模的值命中以前出现的值就会出现环, 而 \(p\) 不会超过 \(\sqrt n\)). 根据生日悖论, 在大约 \(\sqrt p\) 次随机后就会有很大概率出现. 如果出现这种情况, 那么我们就获得了两个值 \(x_a \equiv x_b \pmod p\). 于是它们之间的差是可以被 \(p\) 整除的. 我们取 \(p=\gcd (\left|x_a-x_b\right|,n)\) 即可.
至于这个判环的过程, 我们可以使用神奇的Floyd判环法. 建立两个哨兵 \(x_a\) 和 \(x_b\), 每次令 \(x_a\) 前进一步, 令 \(x_b\) 前进两步, 如果 \(x_a=x_b\), 则说明走到了环上.
但是我们并不知道 \(p\) 是多少, 所以并不能直接判断 \(x_a = x_b\) 来判断 \(\langle x_k \bmod p\rangle\) 是否进入环. 但是根据上文所述, 如果出现了 \(x_a\equiv x_b \pmod p\), 那么就一定出环了, 这个时候 \(p\mid \gcd (\left|x_a-x_b\right|,n)\), 我们便成功获得了一个 \(n\) 的非平凡因子.
不过有时候我们可能会手气不佳, 抽到的 \(c\) 和 \(r\) 生成的 \(\langle x_k \rangle\) 和 \(\langle x_k \bmod p\rangle\) 可能会同时进入环(显然前者不可能比后者进环更快, 最多同时). 不难发现这个环长也是 \(\sqrt p\) 级别的. 但是由于这时候找到的 \(x_a\) 和 \(x_b\) 是相等的, 于是并不能带来什么信息. 在这个时候有两种可能, 第一是 \(n\in \mathbb P\), 第二是脸黑.
普通的Pollard-Rho会到此为止而不提供任何信息.
期望的时间复杂度是 \(O(n^{1/4}\log n)\) 的, 那个 \(\log\) 是 \(\gcd\) 的时候来的.
具体实现
首先上文所述的过程只能用来找非平凡因子而非质因子.
我们把它和 Miller-Rabin 质数测试结合起来就可以进行质因数分解了. 大致流程如下:
- 用 Miller-Rabin 判断当前 \(n\) 是否是质数. 如果是, 丢进答案集合, 跑路.
- 随机选择一对 \(c\) 和 \(r\), 进行上文所述的测试过程.
- 如果找到了一个非平凡因子 \(p\), 递归求解 \(p\) 和 \(n/p\), 然后跑路.
- 否则, 承认自己脸黑, 返回第 2 步再来一次.
代码片段
void PollardRho(intEx n,std::vector<intEx>& factor){
if(MillerRabin(n))
factor.push_back(n);
else while(true){
intEx a,b,c=RandInt(0,n-1);
a=b=RandInt(0,n-1);
auto f=[=](intEx x){
return (Mul(x,x,n)+c)%n;
};
b=f(b);
while(a!=b){
intEx delta=std::abs(a-b);
delta=std::__gcd(delta,n);
if(delta!=1){
PollardRho(delta,factor);
PollardRho(n/delta,factor);
return;
}
a=f(a);
b=f(f(b));
}
}
}
洛谷板子过于SXBK地卡常于是没过TwT...我活该大常数...
LOJ板子更加SXBK地把数据出到了 \(1\times 10^{30}\)...跑路了QAQ...
一般应用没啥问题(吧)...
参考资料
- Pollard's Rho Algorithm - Wikipedia
- Miller-Rabin primality test - Wikipedia
- Birthday problem - Wikipedia
- 算法导论(第三版), 机械工业出版社; 31.8 素数的测试, 31.9 整数的因子分解
本博客已弃用, 新个人主页: https://rvalue.moe, 新博客: https://blog.rvalue.moe