Miller Rabin与Pollard Rho
先写一下Miller Rabin,主要用于判断一个比较大的数是否是质数
解释一个东西。PPT里面在举例子的时候,用\(2^{340}\)为例子,并说明\(2^{170}\)%\(341\)的结果只能是1或者340,这与二次探测定理的\(x\)要小于\(p\)不矛盾,因为PPT说的是\(2^{170}\)%\(341\),利用模的运算法则即可
费马小定理的作用是什么?其实就是提供了多个类似于二次探测定理的等式(不然在哪里去找满足二次探测定理的等式?),然后利用模的运算法则进行二次探测定理的测试
接下来的操作步骤,当结果是1的时候,我们继续就更能增加\(n\)是质数的可能性;如果结果某一次为\(n-1\),就没办法继续用二次探测定理了,所以不得已截止。此时就可以采用多次进行Miller Rabin测试来增加这个算法的正确性。
实现一般都采取倒序(见以下的code),应该是可以避免除法带来的逆元运算(倒序的时候为什么某次结果为\(n-1\)后就可以直接退出循环?因为之后所有的结果都是1了,可以自己手算一下)
bool MillerRabin(ll x)
{
if((x&1)==0) return x==2;
ll u=x-1;
int t=0;
while((u&1)==0) u>>=1,t++;
for(int i=1;i<=8;i++)
{
ll a=rand()%(x-2)+2;
ll v=quickpow(a,u,x);
if(v==1) continue;
bool flag=0;
for(int j=0;j<t;j++)//想一想为什么是小于而不是小于等于
{
if(v==x-1) break;
//注意这里我们是倒序实现的
//如果某一次既不为1也不为n-1
//那么一定不能直接返回0
//因为正常的算法过程是正序的
//如果按照正常的算法过程
//某一次结果为n-1
//那么继续计算的话
//结果是不一定为1或者n-1的
//所以一定要一直计算下去
//否则会错误
v=(__int128)v*v%x;
if(j==t-1) flag=1;
}
if(flag) return 0;
}
return 1;
}
注意Miller—Rabin中间可能要用快速乘法
对于Pollard Rho算法(主要用于将\(N\)分解成两个整数的乘积),下面是一些我的理解
首先了解生日悖论
那么生日悖论在此算法中的作用就是告诉我们,如果在[1,n]中随机生成数,期望在至多\(\sqrt{n}\)次后就会产生相同的两个数
那么在此算法中,我们随机生成[0,n-1]中的数(以下讨论的n都是合数,对于质数可以直接通过Miller-Rabin判断),假设n的一个\(\leq\sqrt{n}\)因子为p,那么根据生日悖论,在至多\(\sqrt{p}\)次后就会生成两个模p意义下相同的两个数。这两个数之差则为p的倍数。设这两个数为a和b,就有\(gcd(a-b,n)>1\),显然\(gcd(a-b,n)\)就可以作为其中一个约数
那么用于生成这个随机数列的函数众所周知,也带来了循环问题(想想随机数函数的内部实现,模运算有循环节)。那么判断环就是用Floyd判圈法。为什么要用这个方法?因为这个方法可以节省空间(可以设想一下,如果普通的判圈,是不是要把出现的数字全部记录下来,这样是很费空间的,甚至会MLE)
那么此时复杂度就已经到达了\(O(n^{\frac{1}{4}}logn)\),代码见下
ll Pollard_Rho(ll N)
{
if (N == 4) // 特判4
return 2;
if (is_prime(N)) // 特判质数
return N;
while (1)
{
ll c = randint(1, N - 1); // 生成随机的c
auto f = [=](ll x) { return ((lll)x * x + c) % N; }; // lll表示__int128,防溢出
ll t = f(0), r = f(f(0));
while (t != r)
{
ll d = gcd(abs(t - r), N);
if (d > 1)
return d;
t = f(t), r = f(f(r));
}
}
}
那么消除log就可以利用累乘的方法。乘法累计的操作就是判断在数列迭代的过程中,是否会存在一个gcd是大于1的,如果存在那么累乘结果的gcd也会大于1,如果不存在那么累乘结果的gcd也是1,这样就可以节省更多的时间。为什么选择128次为一个周期?不太清楚,但是很优秀的选择
ll Pollardrho(ll x)
{
if(x==4) return 2;
while(1)
{
ll c=rand()%(x-1)+1;
ll t=0,r=0,p=1,q;
do{
for(int i=1;i<=128;i++)
{
t=f(t,c,x),r=f(f(r,c,x),c,x);
q=(__int128)p*abs(t-r)%x;
if(t==r||!q) break;//如果遇到环了或者累乘结果为0
//就可以退出循环了
//这样无非是多增加一些判断时间而已
p=q;
}
ll d=gcd(p,x);
if(d>1) return d;
}while(t!=r);
}
}
然后也可以使用倍增优化。上面这个代码有一个问题,就是如果累乘中途的某个结果去算gcd不为0且大于1,但是最终累乘完毕之后累乘结果为0了,就会跳过这次判断,无疑会增加时间复杂度,所以我们循序渐进,按照1次,2次,4次,8次……128次去判断,这样就可以解决这个问题了
ll Pollardrho(ll x)
{
if(x==4) return 2;
while(1)
{
ll c=(ll)rand()%(x-1)+1;
ll t=0,r=0,p=1,q;
int step,goal=1;
bool flag=0;
for(;;goal<<=1)
{
for(step=1;step<=goal;step++)
{
t=f(t,c,x),r=f(f(r,c,x),c,x);
q=(__int128)p*abs(t-r)%x;
if(t==r||!q)
{
flag=1;
break;
}
p=q;
if(step%127==0){
ll d=gcd(p,x);
if(d>1) return d;
}
}
ll d=gcd(p,x);
if(d>1) return d;
if(flag) break;
}
}
}
那么找到一个因子之后,如果找最大质因子?见如下代码,类似于分治
void maxprimefactor(ll x)
{
if(x==1||x<=maxfactor) return;
if(MillerRabin(x)) maxfactor=max(maxfactor,x);
else {
ll p=Pollardrho(x);
while(x%p==0) x/=p;//小优化
maxprimefactor(p);
maxprimefactor(x);
}
}
对于完整的代码,可以去看看洛谷P4718,比较一下两份AC代码的区别和时间
尝试用生日悖论做一下这道题目
前置结论:斐波那契数列的循环节长度不会超过\(6p\),其中\(p\)是模数