大数的质数判定 && 质因子拆分:MR && PR
最近搞这两个算法焦头烂额...但也是有一丝丝收获的。
首先声明,本博客不提供大多数证明。仅仅是根据结论来解释如何构造代码(emmm会用结论就好了嘛)
Miller-Rabin 判定大数
大家听说过费马小定理吧? 就是说如果一个数 p 是质数,那么就有:
$ n^{p-1}≡1 (mod p) $
于是我们大胆猜测:如果一个数 p 对于 任意一个数 n 满足条件:
$ n^{p-1}≡1 (mod p) $
那么这个数是质数。
...
然鹅这个猜想是错的,并且可以十分轻松的证明(打个代码试试就知道了QvQ)。
但是假如 n 是任意正整数,那么这个猜想会不会成立呢?
然鹅事实告诉我们,如此美好的性质并不存在。
比如? 561 。
没错它通过了任意正整数 n 的检测,然鹅它并不是质数。
于是 \(Miller - Rabin\) (注意是两个人)提出了另一种检测质数的方法:二次探测。
对于一个数 p,当有 X(X<p) 满足: $X^{2}≡1 (mod\ p) $ 且 \(X=1 or X=p-1\) 时, p为质数。
证明?我不会啊。。。
看看某大佬的证明:
∵ $X^{2}≡1\ (mod\ p) $
∴ $X^{2}-1≡0\ (mod\ p) $
∴ $(X+1)(X-1)≡0\ (mod\ p) $
又 ∵ \(X<p\)
∴ \(X=1\ or\ X=p-1\)
那么这个检测方法是否恒正确?
答案依旧是:很遗憾,存在反例。
比如 \(46856248255981\)
但是这个特别的数字是 \(1e16\) 范围内唯一的反例。所以可以说 \(Miller-Rabin\) 检测法的正确率已经相当高了。
(毕竟这个特别的数字可以特判掉)
那么这个算法如何实现? 步骤如下:
1.将 \(p-1\) 提取出所有 \(2\) 的因数,假设有 \(tm\) 个。设剩下的部分为 \(ba\) 。
2.枚举(或随机)一个底数 \(a\) 并计算 \(r=a^{ba}(mod p)\)。
3.将 \(r\) 连续平方 \(tm\) 次。如果没有出现过 \(p-1\) ,那么 \(p\) 未通过二次探测,不是质数。
4.否则,若底数已经足够,则跳出。否则回到步骤 2。
那么这样的检测正确率有保障么? 有! 正确率 75% (好低...)
但是我们多取几个随机数检测一下,错误的概率就渐进 \(0\) 了。
(考虑每次判断错误率为 $\dfrac{1}{4} $ ,那么我们判个20 次大概就是 $\dfrac{1}{4^{20}} $ 了 )
那么我们就可以大胆使用这个算法了(但其实这里有优化版的,不过效率不会高太多,所以还是老实写常规版的好)。
namespace Miller_Rabin{
inline ll qmul(ll a,ll p,ll c){ ll res=0;
for(a%=c,p%=c;p;a<<=1,a%=c,p>>=1)
if(p&1) res=(res+a)%c; return res;
} inline ll qpow(ll x,ll p,ll c){ ll s=1;
for(x%=c;p;x=qmul(x,x,c),p>>=1)
if(p&1) s=qmul(s,x,c); return s;
} inline bool check(ll x){
if(x==2) return 1;
if(x==1||(x&1^1)) return 0;
ll ba=x-1,r,ti=0,j;
while(ba&1^1) ba>>=1,++ti;
for(int i=0;i<22;++i){ //普通的话嘛,其实10来次差不多了
r=rand()%(x-1)+1;
r=qpow(r,ba,x);
if(r==1||r==x-1)
continue;
for(j=1;j<=ti;++j){
r=qmul(r,r,x);
if(r==x-1)break;
} if(j>ti)return 0;
} return 1;
}
} using namespace Miller_Rabin; ll ans;
namespace Miller_Rabin{
const ll bace[5]={2,3,7,61,24251};
//这个优化非常有趣,强制转精度的骚操作(关键没有用int128)
inline ll qmul(ll a,ll p,ll c){ static ll d;
d=(long double)a/c*p+1e-8,d=a*p-d*c; return d<0?d+c:d;
} inline ll qpow(ll x,ll p,ll c){ ll res=1;
for(x%=c;p;x=qmul(x,x,c),p>>=1)
if(p&1) res=qmul(res,x,c); return res;
} inline bool check(ll n){
if(n==1||(n&1^1)) return false;
for(int i=0;i<4;++i)
if(bace[i]==n) return 1;
static ll ba,r,j,ti; ba=n-1,ti=0;
for(int i=0;i<5;++i){ //每次判定失败的概率为 1/4 ,(配合二次探测)判个五六次其实就差不多了
ll a=rand()%(n-1)+1;
if(qpow(a,n-1,n)!=1)
return false;
} while(ba&1^1) ba>>=1,++ti;
for(int i=0;i<4;++i){ //然后是二次探测, 只用四个质数就好了
r=qpow(bace[i],ba,n);
if(r==1||r==n-1)continue;
for(j=1;j<=ti;++j){
r=r*r%n; if(r==n-1)break;
} if(j>ti)return 0;
} return 1; //经过了两步检测之后, 基本可以确定是质数
}
}
如果你看不懂的话...附上一些不错的文章:
Pollard-Rho 分解大数
首先 PR 是建立在 MR 基础上的算法(因为分解过程中要判定质数)。
所以,确保你对 MR 算法的基本框架已经熟悉,再往下观看。
这个算法大概讲的就是:
1.判断当前处理的数 n 是否是质数,是的话就记录下来直接返回。
2.随机(又是随机)一个数 p( <=n ),然后经过一系列的运算得到一个新的数字 q(仍不大于 n),然后判断 q 是不是与 n 互质:
(1) 如果非互质的话就可以得到 n 的一个质因子,即 \(gcd(n,q)\)。
(2) 如果互质的话就继续经过一系列的运算得到新的 q ,反复迭代
3.若返回值 \(gcd(n,q)\) 不为 n ,则对 n 分解成功(但并不意味着 \(gcd(n,q)\) 为质数),否则重复步骤 2 直至分解成功
4.此时我们将 \(n\) 分成了两份,设一份为 \(p\) ,则另一份为 \(n/p\),但如步骤 3 所述, p 并非一定是质数,因此我们对这两份数分别递归分解,即将 \(p\) 与 \(n/p\) 置入步骤 1
然后这样我们就可以将 n 分解质因子了。
但是这里最大的疑问就是那个一系列运算究竟是怎样(玄学) 的运算,又是如何保证正确性的。。。
相关证明看 这里 吧。
其次,计算 gcd 之前还有一个判断,具体与 生日悖论有关, 这篇blog 里面有讲
总之会用就好了呗。
so? 复杂度? \(O(n^{\dfrac{1}{4}})\) (这个 lateX 格式是什么鬼..),就是 n 开两次根。
附上代码
//namespace Miller_Rabin 这玩意儿,就是上面的
using namespace Miller_Rabin; ll ans;
ll gcd(ll x,ll y){ return y?gcd(y,x%y):x; }
const int M=(1<<7)-1;
inline ll Abs(ll x){ return x>=0?x:-x; }
inline ll g(ll x,ll n,ll a) { //对,这就是“一系列运算”
ll t=qmul(x,x,n)+a;
return t<n?t:t-n;
} ll Pollard_Rho(ll n){
if(!(n%2)) return 2;
if(!(n%3)) return 3;
ll x=0,y=1,t=1,q=1;
ll c=rand()%(n-1)+1;
for(int k=2;;k<<=1,y=x,q=1){
for(int i=1;i<=k;++i){
x=g(x,n,c),
q=qmul(q,Abs(x-y),n);
if(!(i&M)){ //这里用了生日悖论
t=gcd(q,n);
if(t>1) return t;
}
} if(t>1||(t=gcd(q,n))>1) return t;
}
} void find(ll n){
if(n==1||ans>=n)
return ;
if(check(n)){
ans=n;
return ;
} ll p=n;
while(p>=n)
p=Pollard_Rho(n);
while(n%p==0) n/=p;
find(p),find(n);
}
没了,这就没了。 ヾ(ToT)ByeBye
Judge's Class 越来越水了..._(:з」∠)_