zoukankan      html  css  js  c++  java
  • Pollard_Rho

    Pollard_Rho

    ------

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

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


    目录


    题面

    求一个数的最大质因数.

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

    前置知识:

    Miller_Rabin,快速幂,快速乘,gcd

    1.Miller_Rabin

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

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

    (1)费马小定理

    这个应该比较简单吧.

    (p) 为质数,有

    [a^p equiv apmod{p} ]

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

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

    但是这只是必要不充分条件.存在这样一类强伪素数(p​),满足

    [forall a<p, a^pequiv apmod{p} ]

    这也就意味着无法使用费马小定理将它判断为合数.

    (2)二次探测

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

    证明:

    因为(x^2equiv1pmod{p})

    所以(pmid(x-1)(x+1))

    所以$pmid x-1 quad orquad pmid x+1 $

    所以(xequiv1pmod{p})(xequiv p-1pmod{p})

    证毕.

    那么要如何利用它呢?

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

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

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

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

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

    (2mid p-1​) ,则((x^frac{p-1}{2})^2equiv1pmod{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) e 1(x<N))的个数是(p+q-2).

    所以找一次找出(p,q​) 成功的概率为(frac{p+q-1}{N}​).

    推广到求(N=prod_{i=1}^{n}prime_i^{a_i})的非平凡因子(不是1与本身的因数),找(i)次能找到的概率.

    (p_i=1-prod_{j=0}^i frac{phi(N)-j}{N}​)

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

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

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

    我们构造一个伪随机数,推荐以下代码

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

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

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

    但是,会出现环.因为我们的(x)数列是模意义下生成的,所以可能(exists j e 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.这与分别求是等价的.

    设我们得到的差值为({a_i})

    因为:

    (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((prod_{i=1}^{n}a_i)\%N,N)​)

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

    这样就可以少求很多gcd.

    这里的(a_i​) 在推导过程中只考虑两两互质((gcd(k,N)​)不是完全积性,若不加以说明,①式不恒成立).

    试想(gcd(a_i,a_j) = x(x e 1)​),则(a_i=x*b_i,a_j=x*b_j​)

    (i)(gcd(x,N)=1,)那么(gcd(a_i,N)=gcd(b_i,N),gcd(a_j,N)=gcd(b_j,N))

    所以(gcd(a_i,N)*gcd(a_j,N)=gcd(b_i,N)*gcd(b_j,N)=gcd(b_i*b_j,N))(因为(gcd(b_i,b_j)=1))

    (ii)(gcd(x,N)=k(k e1),​) 那么(gcd(a_i,N)*gcd(a_j,N)​) 必定会出现两次(k​),

    (k^2mid gcd(a_i,N)*gcd(a_j,N)​).

    (gcd(a_i*a_j,N))可能只会出现一次(k)(如果(N)(k)不够的话),

    (kmid gcd(a_i*a_j,N))(k^2 mid gcd(a_i*a_j,N)) (当然,也有可能(k^2 mid gcd(a_i*a_j,N)),如果(N)(k)足够)

    (gcd(a_i*a_j,N)mid gcd(a_i,N)*gcd(a_j,N)),不影响正确性

    而我们分别对(a_i,a_j​) 与N求公因数再相乘时反而会有重复.

    至于累乘多少个差值,这就比较玄学了.这题取的是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的倍数,那么当前差值与N的gcd一定为x的因子.

    ②:( ho)环的长度可能小于127,所以需要在跳出循环时判断.


    Code:

    #include<bits/stdc++.h>
    #define gc getchar
    #define R register int
    #define RG register
    #define int long long
    #define IL inline 
    using namespace std;
    int rd()
    {
        int ans = 0,flag = 1;
        char ch = gc();
        while((ch>'9'||ch<'0')&&ch!='-') ch=gc();
        if(ch == '-') flag=-1;
        while(ch>='0'&&ch<='9') ans+=(ans<<2)+(ans<<1)+ch-48,ch=gc();
        return flag*ans;
    }
    int Ans,n;
    IL int qpow(int x,int a,int mod)
    {
        RG 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>>=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);
    }
    
    IL int qmul(int x,int y,int mod)
    {
        return (x*y-(long long)((long double)x/mod*y)*mod+mod)%mod;
    }
    inline int gcd(int x,int y)
    {
        return y?gcd(y,x%y):x;	
    }
    
    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;
        }
    
    }
     main()
    {
        //freopen("Divide.in","r",stdin);
        //freopen("Divide.out","w",stdout);
        ios::sync_with_stdio(false);
        cin>>n;
        for(R i=1;i<=n;i++)
        {
            RG int t;
            cin>>t;
            if(Test(t))
            {
                cout<<"Prime"<<endl;
                continue;
            }
            Ans=0;
            while(Ans==0)
                Pollard_Rho(t);
            cout<<Ans<<endl;
        }
        return 0;
    }
    
    

    基本没卡常.


    复杂度

    个人认为这一部分相对不是很重要,毕竟作为一名(REIO) ,知道如何用算法就可以了,感兴趣的同学可以看一下不保证正确

    而且说实话,这种随机算法的复杂性我不会分析

    为什么是(O(n^frac{1}{4})​) 呢?

    之前有提到:

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

    那么满足((N,x) e 1(x<N)​)的个数是(p+q-2​).

    所以找一次找出(p,q​) 成功的概率为(frac{p+q-1}{N}​).

    找i次找到的概率(P_i=1-prod_{j=0}^i frac{(N-p-q+2)-j}{N}​)

    我们用(sqrt{N})来近似$p,q $ (这个上界是比较松的(p+qge2sqrt{pq}=2sqrt{N}))

    那么

    (P_iapprox1-prod_{j=0}^i frac{(N-2sqrt{N}+2)-j}{N})

    (approx 1-prod_{j=0}^{i}frac{sqrt{N}-j}{sqrt{N}}​)(这个还是上界)

    (t=sqrt{N}​).

    那么(P_iapprox1-prod_{j=0}^{i}frac{t-j}{t})

    (这个式子很熟悉吧)

    而生日悖论告诉我们,当(i)取到(sqrt{t})(也就是(N^frac{1}{4}))时,(P_i)就会大于(frac{1}{2}​).

    所以说复杂度为(O(N^frac{1}{4})​).

    另一种说法是(O(sqrt{p})​)(p为N的较小因数),原因应该也类似.


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

  • 相关阅读:
    【NOIP】提高组2015 神奇的幻方
    【BZOJ】1087: [SCOI2005]互不侵犯King
    【NOIP】提高组2005 过河
    【NOIP】提高组2012 借教室
    【vijos】P1083 小白逛公园
    【vijos】P1659 河蟹王国
    【vijos】P1448 校门外的树
    【vijos】P1066 弱弱的战壕
    【TYVJ】P1039 忠诚2
    【TYVJ】P1038 忠诚
  • 原文地址:https://www.cnblogs.com/Zenyz/p/10543400.html
Copyright © 2011-2022 走看看