质因数分解 --算法竞赛专题解析(19)

本系列文章将于2021年整理出版,书名《算法竞赛专题解析》。
前驱教材:《算法竞赛入门到进阶》 清华大学出版社
网购:京东 当当   想要一本做者签名书?点我
若有建议,请加QQ 群:567554289,或联系做者QQ:15512356
本文在公众号同步,阅读更方便:算法专辑
公众号还有暑假福利,免费连载做者的书:胡说三国html

   任何一个正整数 n n 均可以惟一分解为有限个素数的乘积: n = p 1 c 1 p 2 c 2 . . . p m c m n = p_1^{c_1}p_2^{c_2}...p_m^{c_m} ,其中 c i c_i 都是正整数, p i p_i 都是素数且从小到大。
  质因数分解有重要工程意义。在密码学中,须要对高达百位以上的十进制数分解质因子,所以发明了不少高效率的方法1。不过,大数的质因子分解是个难题,比寻找大素数要可贵多,密码算法RSA就利用了大数难以分解的原理。web

一、用试除法分解质因子

   分解质因子也能够用前面提到的试除法。求 n n 的质因子:
   (1)第一步,求最小质因子 p 1 p_1 。逐个检查从2到 n \sqrt n 的全部素数,若是它能整除n,就是最小质因子。而后连续用 p 1 p_1 n n ,目的是去掉 n n 中的 p 1 p_1 ,获得 n 1 n_1
   (2)第二步,再找 n 1 n_1 的最小质因子。逐个检查从 p 1 p_1 n 1 \sqrt {n_1} 的全部素数。从 p 1 p_1 开始试除,是由于 n 1 n_1 没有比 p 1 p_1 小的素因子,并且 n 1 n_1 的因子也是 n n 的因子。
   (3)继续以上步骤,直到找到全部质因子。
   最后,通过去除因子的操做后,若是剩下一个大于1的数,那么它也是一个素数,是 n n 的最大质因子。这种状况能够用一个例子说明。大于 n \sqrt n 的素数也多是 n n 的质因子,例如6119 = 29*211,找到29后,由于29 ≥ 211 \sqrt {211} ,说明211是素数,也是质因子。
   试除法的复杂度是 O ( n ) O(\sqrt n) ,效率很低。不过,在算法竞赛中,数据规模不大,因此通常就用试除法。
   下面是试除法的代码2。由于试除法的效率不高,因此 n n 用int型,没有用long long。算法

int p[20];  //p[]记录因子,p[1]是最小因子。一个int数的质因子最多有10几个
int c[40];  //c[i]记录第i个因子的个数。一个因子的个数最多有30几个

void factorization(int n){
    int m = 0;
    for(int i = 2; i*i <= n; i++)
        if(n%i == 0){
           p[++m] = i, c[m] = 0;
           while(n%i == 0)            //把n中重复的因子去掉
              n/=i, c[m]++;    
        }
    if(n>1)                           //没有被除尽,是素数
       p[++m] = n, c[m] = 1;  
}

二、用Pollard_rho启发式方法分解质因子

  试除法的复杂度是 O ( n ) O(\sqrt n) ,也就是说,对到 B B 的整数进行试除,能够彻底得到到 B 2 B^2 的任意数的因子分解;用本节的pollard_rho算法,用一样的工做量,能够对到 B 4 B^4 的数进行因子分解3。须要指出的是,pollard_rho算法也仍然是一种低效的方法,比试除法好一点点,只能在算法竞赛的小规模数据中用用。app

  思考一个问题:如何快速找到一个大数的因子?不能像试除法那样从小到大一个个检查,太慢了。能够挑一些数来“试”,运气好说不定就碰到一个。这就是pollard_rho算法的思路,它使用了一个“随机”的方法来找。算法的主要内容只有2个:
  (1)“随机”函数。实际上不是随机,而是一个启发函数: x i = ( x i 1 2 + c )   m o d   n x_i = (x_{i-1}^2 + c)\ mod\ n ,其中 x x 的初值 x 1 x_1 c c 是随机数。计算的结果是生成了一个 x x 序列,这个序列的前一部分 x 1 x 2 . . . x j 1 x_1,x_2,...,x_{j-1} 不重复,后面的 x j x j + 1 . . . x i x_j,x_{j+1},...,x_i 会重复并造成回路。rho指希腊字母" ρ \rho ",不重复的序列是 ρ \rho 的“尾巴”,重复的回路是 ρ \rho 的“身体”。ide

图1 rho的“尾巴”和“身体”

  (2)计算 n n 的一个因子。计算 d = g c d ( y x i , n ) d = gcd(y - x_i, n) ,其中y是第 2 k 2^k x x ,即第一、二、四、八、…个,见上图中划线的 x x 。若是d ≠ 1且d ≠ n,d就是n的一个因子,缘由很简单,gcd是求最大公约数,因此d确定是n的因子。
  从上面的描述能够看出,pollard_rho算法极为简单,读者可能怀疑它是否真的有效。确实,在一次 x x 序列中,极可能计算不出因子,须要屡次“随机”的 x x 序列才能算出一个因子。使人惊讶的是,这个算法的效果还不错,它能够用 O ( p ) O(\sqrt p) 次计算找到 n n 的一个小因子 p p
  pollard_rho的编码很是简单,见下面代码中的pollard_rho()函数。因为执行一次pollard_rho()只返回一个因子,要获得全部的因子,须要再写一个findfac()函数屡次调用pollard_rho(),递归求得全部素因子。svg

poj 1811 部分代码(pollard_rho)
//poj 1811题:输入一个整数n,2<=N<2^54,判断它是否为素数,若是不是,输出最小素因子。

typedef long long ll;

ll Gcd (ll a,ll b){  return b? Gcd(b, a%b):a;}

ll pollard_rho (ll n){       //返回一个因子,不必定是素因子
    ll i=1, k=2;
	ll c = rand()%(n-1)+1;
    ll x = rand()%n;
    ll y = x;
    while (true){
        i++;
        x = (mult_mod(x,x,n)+c) % n;   //mult_mod(x,x,n)功能是(x*x) mod n
        ll d = Gcd(y>x?y-x:x-y, n);    //重要:保证gcd的数大于等于0
        if (d!=1 && d!=n) return d;    //算出一个因子 
        if (y==x) return n;            //已经出现过,直接返回
        if (i==k) { y=x; k=k<<1;}
    }
}
void findfac (ll n){                   //找全部的素因子
    if (miller_rabin(n)) {             //用miller_rabin判断是否为素数
        factor[tol++] = n;             //存素因子
        return;
    }
    ll p = n;
    while (p>=n) 
		p = pollard_rho(p);            //找到一个因子
    findfac(p);                        //继续寻找更小的因子
    findfac(n/p);
}

  1. 试除法很低效,有不少更好的分解质因子的方法,参考《初等数论及其应用》93页。 ↩︎函数

  2. 代码改写自《算法竞赛进阶指南》河南电子音像出版社,李煜东,137页。 ↩︎编码

  3. 《算法导论》Thomas H.Cormen等著,潘金贵等译,机械工业出版社,551页。 ↩︎spa