\(PollardRho\) 算法总结:
Pollard Rho是一个非常玄学的算法,用于在\(O(n^{1/4})\)的期望时间复杂度内计算合数n的某个非平凡因子(除了1和它本身以外能整除它的数)。事实上算法导论给出的是\(O(\sqrt p)\),\(p\)是\(n\)的某个最小因子,满足\(p\)与\(n/p\)互质。但是这些都是期望,未必符合实际。但事实上Pollard Rho算法在实际环境中运行的相当不错。
一些与\(PollardRho\) 算法经常一起考的东西:
\(PollardRho\) 算法:
对于一个比较小的数(\(n\leq10^9\)),我们直接暴力就行.但太大了我们就必须考虑一下随机算法了。
对于一个非常大的合数\(n\leq 10^{18}\)(如果可能是质数,我们可以用Miller Rabin判一下)我们要求 \(n\) 的某一个非平凡因子,如果 \(n\) 的质因子很多(就是约数很多)我们也可以暴力随机弄一弄,但如果是一些(像由两个差不多的而且很大的质数乘得的 \(n\) )它的非平凡因子就只有两个而且 \(n\) 本身还很大,此时如果我们随机的话复杂度\(O(n)\),这个太难接受了,所以我们想办法优化一下。
1.我们求\(gcd\)
如果我们直接随机求 \(n\) 的某一个约数复杂度很高,我们可以考虑求一下 \(gcd\) 。因为我们可以保证一个合数它绝对存在一个质因子小于\(\sqrt n\) ,所以在 \(n\) 就存在至少\(\sqrt n\) 个数与 \(n\) 有大于一的公约数。于是我们随机的可能性就提高到了\(O(\sqrt n)\) 。
2. 玄学随机法:\(x^2+c \quad _{x=rand(),c=rand()}\)
其实网上大多都叫这种方法为生日悖论,有兴趣的可以去看看,但这个其实是可以通过数学概率证明的。但博主觉得这个方法跟我们的\(PollardRho\) 没有关联。在1中我们用gcd的方案,现在我们考虑有没有办法能够快速的找到这些与 \(n\) 有公约数的数(或者说找到一种随机生成方法,使随机生成这类我们需要的数的概率变大一点)。
这个随机生成方法最终被\(Pollard\)研发出来了:就是上面那个式子! 我们定义\((y=x\quad x=x*x+c \quad g=gcd(y-x,p))\) 为一次操作,我们发现 \(g>1\) (就是找到我们需要的数)的几率出奇的高,根据国际上大佬的疯狂测试,在 \(n\) 中找到一对 \(y-x\) 使两者有公约数的概率接近\(n^{\frac{1}{4}}\) ,不得不说太玄学又太优秀了!
但是这种随机生成方法虽然优秀,也有一些需要注意之处,比如有可能会生成一个环,并不断在这个环上生成以前生成过一次的数,所以我们必须写点东西来判环:
- 我们可以让y根据生成公式以比x快两倍的速度向后生成,这样当y再次与x相等时,x一定跑完了一个圈且没重复跑多少!
- 我们可以用倍增的思想,让y记住x的位置,然后x再跑当前跑过次数的一倍的次数。这样不断让y记住x的位置,x再往下跑,因为倍增所以当x跑到y时,已经跑完一个圈,并且也不会多跑太多(这个必须好好想一想,也可以看看代码如何实现的)(因为这种方法会比第一种用的更多,因为它在\(PollardRho\) 二次优化时还可以起到第二个作用)(博主这里就只给第二种的代码吧)
inline ll rho(ll n){
ll x,y,c,g; rg i,j;
x=y=rand(); c=rand();//初始化
i=0,j=1;//倍增初始化
while(++i){
x=(ksc(x,x,n)+c)%n;//x只跑一次
if(x==y)break;//跑完了一个环
g=gcd(Abs(y-x),n);//求gcd(注意绝对值)
if(g>1)return g;
if(i==j)y=x,j<<=1;//倍增的实现
}
}
\(PollardRho\) 算法的二次优化:
我们发现其实\(PollardRho\) 算法其实复杂度不止\(O(n^{\frac{1}{4}})\),它每一次操作都会求一次\(gcd\),所以复杂度会变成\(O(n^{\frac{1}{4}}*log)\) 我们发现这个 \(log\) 实在有些拖慢速度(虽然实际上也很快了)。于是一个很奇妙的优化诞生了!
二次优化:我们发现我们在每一次生成操作中,乘法之后所模的数就是我们的\(n\),而我们要求的就是\(n\)的某一个约数!也就是说我们现在的模数并不是一个质数!而根据取模的性质:如果模数和被模的数都含有一个公约数,那么这次模运算的结果必然也会是这个公约数的倍数!!!所以如果我们将若干个\((y-x)\) 乘到一起,因为中途模的是 \(n\) ,所以如果我的若干个\((y-x)\)中有一个与n有公约数,最后的结果定然也会含有这个公约数!所以我们完全可以多算几次\((y-x)\)的乘积在来求\(gcd\) (一般连续算127次再求一次gcd(这个应该有大佬测试过了)),这样我们的复杂度就能降一个\(log\) 级别,跑的飞快!
对原算法的一些影响:任何一个优化都是要考虑其对原算法的影响的。这个优化也会有一些影响:首先,因为我们会等大概127次后再去gcd,然而我们有可能在生成时碰上一个环,我们有可能还没生成127次就跳出这个环了,这样就无法得出答案;其次,我们的 \(PollardRho\) 算法实在太玄学优秀了,可能我们跑127次之后,所有\((y-x)\) 的乘积就变成了n的倍数(模\(n\) 意义下得到 \(0\) ),所以我们不能完全就只呆板的等127次在取模。
那怎么办呢?(博主在上文就已经提到过了:倍增时我们的 \(i\) 和 \(j\) 会在二次优化时起到另一种作用。)没错!就是倍增!我们可以用倍增,分别在生成(1次,2次,4次,8次,16次,32次......)之后进行 \(gcd\) !这样就可以完全避免上述的两个影响,而且我们是倍增来gcd的这对我们的复杂度影响不大!!!!!然后我们看代码吧!
inline ll rho(ll p){//求出p的非平凡因子
ll x,y,z,c,g; rg i,j;//先摆出来(z用来存(y-x)的乘积)
while(1){//保证一定求出一个因子来
y=x=rand()%p;//随机初始化
z=1; c=rand()%p;//初始化
i=0,j=1;//倍增初始化
while(++i){//开始玄学生成
x=(ksc(x,x,p)+c)%p;//可能要用快速乘
z=ksc(z,Abs(y-x),p);//我们将每一次的(y-x)都累乘起来
if(x==y||!z)break;//如果跑完了环就再换一组试试(注意当z=0时,继续下去是没意义的)
if(!(i%127)||i==j){//我们不仅在等127次之后gcd我们还会倍增的来gcd
g=gcd(z,p);
if(g>1)return g;
if(i==j)y=x,j<<=1;//维护倍增正确性,并判环(一箭双雕)
}
}
}
}
然后是两道道例题,都有点难度的。
\(code1:\)
博主给出的样例代码是对应多组数据的,对于每个数字检验是否是质数,是质数就输出Prime
;如果不是质数,输出它最大的质因子是哪个。对应洛谷P4718 【模板】Pollard-Rho算法
#include<iostream>
#include<cstdio>
#include<iomanip>
#include<algorithm>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<queue>
#include<vector>
#include<stack>
#include<map>
#include<set>
#define ull unsigned long long
#define lb long double
#define ll long long
#define rg register int
using namespace std;
int n;
ll ans,m;
inline ll qr(){//快读
char ch;
while((ch=getchar())<'0'||ch>'9');
ll res=ch^48;
while((ch=getchar())>='0'&&ch<='9')
res=res*10+(ch^48);
return res;
}
inline ll Abs(ll x){return x<0?-x:x;}//取绝对值
inline ll gcd(ll x,ll y){//非递归求gcd
ll z;
while(y){z=x;x=y;y=z%y;}
return x;
}
inline ll ksc(ull x,ull y,ll p){//O(1)快速乘(防爆long long)
return (x*y-(ull)((lb)x/p*y)*p+p)%p;
}
inline ll ksm(ll x,ll y,ll p){//快速幂
ll res=1;
while(y){
if(y&1)res=ksc(res,x,p);
x=ksc(x,x,p); y>>=1;
}return res;
}
inline bool mr(ll x,ll p){//mille rabin判质数
if(ksm(x,p-1,p)!=1)return 0;//费马小定理
ll y=p-1,z;
while(!(y&1)){//二次探测
y>>=1; z=ksm(x,y,p);
if(z!=1&&z!=p-1)return 0;
if(z==p-1)return 1;
}return 1;
}
inline bool prime(ll x){ if(x<2)return 0;//mille rabin判质数
if(x==2||x==3||x==5||x==7||x==43) return 1;
return mr(2,x)&&mr(3,x)&&mr(5,x)&&mr(7,x)&&mr(43,x);
}
inline ll rho(ll p){//求出p的非平凡因子
ll x,y,z,c,g; rg i,j;//先摆出来(z用来存(y-x)的乘积)
while(1){//保证一定求出一个因子来
y=x=rand()%p;//随机初始化
z=1; c=rand()%p;//初始化
i=0,j=1;//倍增初始化
while(++i){//开始玄学生成
x=(ksc(x,x,p)+c)%p;//可能要用快速乘
z=ksc(z,Abs(y-x),p);//我们将每一次的(y-x)都累乘起来
if(x==y||!z)break;//如果跑完了环就再换一组试试(注意当z=0时,继续下去是没意义的)
if(!(i%127)||i==j){//我们不仅在等127次之后gcd我们还会倍增的来gcd
g=gcd(z,p);
if(g>1)return g;
if(i==j)y=x,j<<=1;//维护倍增正确性,并判环(一箭双雕)
}
}
}
}
inline void prho(ll p){//不断的找他的质因子
if(p<=ans)return ;//最优性剪纸
if(prime(p)){ans=p;return;}
ll pi=rho(p);//我们一次必定能求的出一个因子,所以不用while
while(p%pi==0)p/=pi;//记得要除尽
return prho(pi),prho(p);//分开继续分解质因数
}
int main(){
n=qr(); srand(time(0));//随机数生成必备!!!
for(rg i=1;i<=n;++i){
ans=1; prho((m=qr()));
if(ans==m)puts("Prime");
else printf("%lld\n",ans);
}
return 0;
}
\(code2:\)
根据这道题的答案性质,我们知道这个数在除了1以外还有三个约数,仔细想一下不难发现这类数最多只能有两个质因子,而这两个质因子分别就是它的两个约数,而第三个约数自然就是它自己了!(当然还有可能出现质数的三次方这类特殊的数,所以我们要特判一下)
所以我们的思路就是,先求出 \(n\) 的一个小于它的约数,然后我们可以用这个约数算出 \(n\) 的另一个约数,然后我们判一下它是不是质数的三次方这类特殊的数(是就可以直接输出答案了),不是的话我们判断这两个约数是否分别是不同的质因子,是就能输出了,不是就说明这不是\(D\_num\)
#include<iostream>
#include<cstdio>
#include<iomanip>
#include<algorithm>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<queue>
#include<stack>
#include<vector>
#include<map>
#include<set>
#define ll long long
#define db double
#define rg register int
#define cut {puts("is not a D_num");continue;}//这个可以看一看
using namespace std;
ll n;
inline ll qr(){
char ch;
while((ch=getchar())<'0'||ch>'9');
ll res=ch^48;
while((ch=getchar())>='0'&&ch<='9')
res=res*10+(ch^48);
return res;
}
inline ll Abs(ll x){return x<0?-x:x;}
inline ll gcd(ll x,ll y){
ll z;
while(y){z=x,x=y,y=z%y;}
return x;
}
inline ll ksm(ll x,ll y,ll p){
ll res=1;
while(y){
if(y&1)res=(__int128)res*x%p;
x=(__int128)x*x%p; y>>=1;
}return res;
}
inline bool mr(ll x,ll p){
if(ksm(x,p-1,p)!=1)return 0;
ll y=p-1,z;
while(!(y&1)){
y>>=1; z=ksm(x,y,p);
if(z!=1&&z!=p-1)return 0;
if(z==p-1)return 1;
}return 1;
}
inline bool prime(ll p){ if(p<2)return 0;
if(p==2||p==3||p==5||p==7||p==43)return 1;
return mr(2,p)&&mr(3,p)&&mr(5,p)&&mr(7,p)&&mr(43,p);
}
inline ll rho(ll p){
ll x,y,z,c,g; rg i,j;
while(1){
y=x=rand()%p;
z=1,c=rand()%p;
i=0,j=1;
while(++i){
x=((__int128)x*x+c)%p;
z=(__int128)z*Abs(y-x)%p;
if(x==y)break;
if(i%72||i==j){
g=gcd(z,p);
if(g>1&&g!=p)return g;
if(i==j)y=x,j<<=1;
}
}
}
}
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
srand(time(0)); ll p,q;
while(scanf("%lld",&n)!=EOF){
if(prime(n)||n<=2)cut;
p=rho(n); q=n/p;
if((__int128)p*p==q||(__int128)q*q==p)//这题很卡细节的
{printf("%lld %lld %lld\n",p,q,n);continue;}
if(!prime(p)||!prime(q)||p==q)cut;//注意两个相等也不行
printf("%lld %lld %lld\n",min(p,q),max(p,q),n);
}
return 0;
}