Pollard_Rho

Pollard_Rho


\(Pollard Rho ​\)(在此简称PR)可以用来在 \(O(N^{\frac{1}{4}})​\) 的时间内分解质因数.

(这个算法是\(Pollard​\)提出来的;算法中会涉及到一个环,它的形状为\(''\rho''​\),所以叫\(Pollard Rho​\) )


题面

求一个数的最大质因数.

这题不需要卡常,不需要卡常,不需要卡常!!!

前置知识:

Miller_Rabin,快速幂,快速乘,gcd

1.Miller_Rabin

​ Miller_Rabin(在此简称MR)是一个高效(\(O(log_2 N)​\))判素数的随机算法,是PR的基础,十分重要

​ 而MR也有两个前置知识:费马小定理,二次探测定理

(1)费马小定理

​ 这个应该比较简单吧.

\(p​\) 为质数,有

\[ a^p \equiv a\pmod{p} \]

​ 我们现在要验证的数为\(p\) ,那么可以选取一些数作为\(a\),带入上式,分别检验是否满足费马小定理.

​ 只要有一个\(a\) 不满足 \(a^p \equiv a\pmod{p}\) ,那么p为合数.

​ 但是这只是必要不充分条件.存在这样一类强伪素数\(p\),满足
\[ \forall a<p, a^p\equiv a\pmod{p} \]
​ 这也就意味着无法使用费马小定理将它判断为合数.

(2)二次探测

\(x^2\equiv1\pmod{p}\),若\(p\)为质数,则\(x\equiv1\pmod{p}\)\(x^2\equiv p-1\pmod{p}\)

证明:

因为\(x^2\equiv1\pmod{p}\)

所以\(p\mid(x-1)(x+1)\)

所以$p\mid x-1 \quad or\quad p\mid x+1 $

所以\(x\equiv1\pmod{p}\)\(x\equiv p-1\pmod{p}\)

证毕.

​ 那么要如何利用它呢?

​ 我们要检测的数仍然为\(p\),然后再选定一个数\(x\).

​ 此时\(p\)已经满足了费马小定理(否则会被直接判合数),即:\(x^{p-1} \equiv 1\pmod{p}\)

​ (注意,这个式子中的模数在递归求解的过程中是始终不变的,但指数会改变)

​ 这个式子的形式是不是和二次探测定理中的形式很相似?

​ 实际上,我们可以利用二次探测来继续判断质数.

​ 若\(2\mid p-1\) ,则\((x^\frac{p-1}{2})^2\equiv1\pmod{p}\),判断\(x^\frac{p-1}{2}\)\(p\)意义下的值.

​ a. 若既不是1,也不是p-1,那么说明p是合数,返回false.

​ b. 若是1,则继续递归 \(x^\frac{p-1}{2}\)

​ c.若为p-1,那么不能利用二次探测继续递归,说明目前无法验证p为合数,返回true.

​ 多取几个x来测试就可以了.

Code:

IL int qpow(int x,int a,int mod)//快速幂
{
    int ans = 1;
    while(a)
    {
        if(a&1) ans=ans*x%mod;
        a>>=1,x=x*x%mod;
    }
    return ans;
}
IL bool test(int x,int p)//费马小定理
{
    return qpow(x,p-1,p)==1;
}
IL bool Miller_Rabin(int x,int p)
{
    if(!test(x,p)) return false;
    int k=p-1;
    while(!(k&1))
    {
        //k/=2;
        k>>=1;
        RG int t=qpow(x,k,p);
        if(t!=1&&t!=p-1) return false;
        if(t==p-1) return true;
    }
    return true;
}
IL bool Test(int p)
{
    if(p==1) return false;
    if(p==2||p==3||p==5||p==7||p==11) return true;
    return Miller_Rabin(2,p)&&Miller_Rabin(3,p)&&Miller_Rabin(5,p)&&Miller_Rabin(7,p);
    //取不同的x来测试,提高正确率
}

2.快速幂,快速乘

前者就不说了,主要是快速乘.(不过一般快速幂里面的乘法也要用到快速乘)

它的用处是计算两个longlong级别的数的模意义下的乘法

原理:
\[ a\%b=a-[a/b]*b \]
网上搜\(O(1)\)快速乘,这里不解释了.

Code:

IL int qmul(int x,int y,int mod)
{
    return (x*y-(long long)((long double)x/mod*y)*mod+mod)%mod;
}

3.gcd

我用的是辗转相除,但听说用二进制更快?但是好像也只是常数级别的优化(后文会提到一个很重要的东东)

IL int gcd(int x,int y)
{
    return y?gcd(y,x%y):x;  
}

了解了上述知识后,你就可以开始做这题了QAQ


做法

方法一:试除法

这个很简单,就不说了.

方法二:rand

我要分解的数为\(N\),我在区间\([1,N]\)中rand一个数,看它是不是N的因数.

(很搞笑吧)

方法三:Birthday Trick 优化的rand(正解)

嗯,我们从\([1,N]​\)中rand两个数,那么它们的差为N的因数的可能性会大大提升.

但是因为要两两作差,所以没有优化.

但是我们可以退而求其次,使这两个数的差不一定要满足与N的因数完全相等,而是它们的最大公因数不等于一.

那么这个时候我们成功的几率就很高了.

以下纯粹是口胡

若一个数\(N=p*q​\) (p,q均为质数)

那么满足\((N,x)!=1(x<N)\)的个数是\(p+q-1\).

而两个数作差等于某一数的概率约为\(\frac {1}{\sqrt{N}}\),

所以成功的概率为\(\frac{p+q-1}{\sqrt{N}}\).

-----口胡结束-----

如果这样做,据说要选\(N^{\frac{1}{4}}​\)个数,无法存储.

因此,我们必须每次运算只记录两个数.

我们构造一个伪随机数,推荐以下代码(虽然不知道为什么QAQ)

 t1=(qmul(t1,t1,x)+b);

就是\(x_i=x_{i-1}^2+C​\)(C为常数).

比较\(x\)数列的相邻两项.

但是,会出现环.因为我们的\(x\)数列是模意义下生成的,所以可能\(\exists j\ne i,x_i=x_j\).

那么如何解决呢?

用hash吗?不不不,那太麻烦了.

以下内容为引用

那么,如何探测环的出现呢?
一种方法是记录当前产生过的所有的数$x_1 , x_2 , ... ... x_n \(,并检测是否存在\)x_l = x_n (l < n)$。
在实际过程中,当 n 增长到一定大小时,可能会造成的内存不够用的情况。
另一种方法是由Floyd发明的一个算法,我们举例来说明这个聪明而又有趣的算法。
假设我们在一个很长很长的圆形轨道上行走,我们如何知道我们已经走完了一圈呢?
当然,我们可以像第一种方法那样做,但是更聪明的方法是让 A 和 B 按照 B 的速度是
A 的速度的两倍从同一起点开始往前走,当 B 第一次敢上 A 时(也就是我们常说的套圈) ,
我们便知道,B 已经走了至少一圈了。

while ( b != a )
a = f(a); // a runs once
b = f(f(b)); // b runs twice as fast.
p = GCD( | b - a | , N);

就这样,我们可以较快的找出\(N​\)的两个非平凡因子p,q.

递归求解,直到本身为素数返回即可.

代码如下:


void Pollard_Rho(int x) 
{
    if(Test(x))
    {
        Ans=max(x,Ans);
        return; 
    }
    int t1=rand()%(x-1)+1;
    int t2=t1,b=rand()%(x-1)+1;
    t2=(qmul(t2,t2,x)+b)%x;//要用快速乘
    int i=0;
    while(t1!=t2)
    {
        ++i;
        int t=gcd(abs(t1-t2),x);
        if(t!=1&&t!=x) 
        {
            Pollard_Rho(t),Pollard_Rho(x/t);    
        }
        t1=(qmul(t1,t1,x)+b)%x;
        t2=(qmul(t2,t2,x)+b)%x;
        t2=(qmul(t2,t2,x)+b)%x;
    }
}

正解优化

我在这里提一个最重要的优化,加上后基本不需要卡常就可以跑进\(2500ms​\).

我们要频繁地求gcd,可不可以更快地求呢?

可以!

我们可以将若干个差值累积乘到一起,再求gcd.这与分别求是等价的.

证明:

\(gcd(\prod_{i=1}^{n}a_i,N)=\prod_{i=1}^n gcd(a_i,N)​\).(这个应该是显然吧)

又有\(gcd(\prod_{i=1}^{n}a_i,N)=gcd(N,\prod_{i=1}^{n}a_i\%N)​\)

所以\(gcd(N,\prod_{i=1}^{n}a_i\%N)=\prod_{i=1}^n gcd(a_i,N)\)

这样就可以少求很多gcd.

至于累乘多少个差值,这就比较玄学了.这题取的是127(可能是面向数据编程?QAQ)

改进代码如下:

void Pollard_Rho(int x) 
{
    if(Test(x))
    {
        Ans=max(x,Ans);
        return; 
    }
    int t1=rand()%(x-1)+1;
    int t2=t1,b=rand()%(x-1)+1;
    t2=(qmul(t2,t2,x)+b)%x;
    int p=1;
    int i=0;
    while(t1!=t2)
    {
        ++i;
        p=qmul(p,abs(t1-t2),x);
        if(p==0) //①
        {
            int t=gcd(abs(t1-t2),x);
            if(t!=1&&t!=x) 
            {
                Pollard_Rho(t),Pollard_Rho(x/t);    
            }
            return;
        }
        if(i%127==0)//②
        {
            p=gcd(p,x);
            if(p!=1&&p!=x)
            {
                Pollard_Rho(p),Pollard_Rho(x/p);
                return;
            }
            p=1;
        }
        t1=(qmul(t1,t1,x)+b)%x;
        t2=(qmul(t2,t2,x)+b)%x;
        t2=(qmul(t2,t2,x)+b)%x;
    }
    p=gcd(p,x);
    if(p!=1&&p!=x)
    {
        Pollard_Rho(p),Pollard_Rho(x/p);
        return;//③
    }

}

还是有两个要注意的地方:

①:p为0,说明乘上当前差值后变为了x的倍数,那么当前差值一定为x的因子.

③:\(\rho\)环的长度小于127,需要在跳出循环时判断.


本文参考了A Quick Tutorial on Pollard's Rho Algorithm,百度百科-生日悖论

猜你喜欢

转载自www.cnblogs.com/Zenyz/p/10543400.html