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\)是模数

posted @ 2023-09-03 23:36  最爱丁珰  阅读(3)  评论(0编辑  收藏  举报