分解质因数-Pollard‘s Rho


  随便写写,不喜勿喷。


质数

Prime


  对于整数 n n n,若 n ≠ − 1 , 0 , 1 n \neq -1,0,1 n=1,0,1,且 n n n 除去显然因数 ( ± n (\pm n (±n ± 1 ) \pm 1) ±1) 外没有其他因数,则我们称 n n n 为质数。


质数的判定

Primality test


  又称 素性测试,

  是一种判定某个数是否为质数的方法。

  素性测试有两种,

  1.确定性测试 \ 确定型算法
  2.概率性测试 \ 随机型算法

  确定性测试可绝对确定一个数是否为质数,概率性测试则有较小的概率错误的将合数判断为质数,因此,通过了概率性测试的数字被称为 可能质数,通过了概率性测试的合数被称为 伪质数,常见的有费马伪质数。

  下面将简单带过一下,暴力测试(试除法),

  引入费马小定理,以及两个基于费马小定理的随机型算法 费马素性测试、 M i l l e r \mathrm{Miller} Miller- R a b i n \mathrm{Rabin} Rabin 素性测试。


试除法

Trial Division


  一个数的因子总是成对出现的,因此对于正整数 n n n,它的最小因子不会大于 n \sqrt n n

  若 2 ∼ n 2 \sim \sqrt n 2n 中不存在一个整数 k k k,使得 k ∣ n k \mid n kn,那么我们就可以确定的认为 n n n 就是质数。

bool is_prime(int n) {
    
    
    for (int i = 2; i <= sqrt(n); ++i)
        if (n % i == 0) return 0;
    return 1;
}

费马小定理

Fermat’s little theorem


  若 p p p 是质数, a a a 为整数且 a a a 不是 p p p 的倍数,则有: a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv1 \pmod p ap11(modp)

  费马小定理也是欧拉定理的特殊形式。


Fermat 素性测试


  费马测试是最为简单的概率性测试,

  其思想是,不断的在 2 ∼ n − 1 2 \sim n-1 2n1 中,选择一个 a a a 作为基,判断对于整数 a a a,是否都有 a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv1 \pmod p ap11(modp)

int mod_pow(int a, int b, int p) {
    
    
    int pow = 1;
    while (b) {
    
    
        if (b & 1) pow = pow * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return pow;
}

int test_time = 50;

bool is_prime(int n) {
    
    
    for (int i = 0; i < test_time; ++i)
        if (mod_pow(1 + rand() % (n - 1), n - 1, n) != 1) return 0;
    return 1;
}

  在上面程序中,随机选择了 a a a,极大程度上避免了错误判定的发生,但有一类数无法被费马测试准确判断,

  这类数被称为 卡迈克尔数,也被称为 费马伪素数,

  其定义为对于合数 n n n,对于任意整数 a a a gcd ⁡ ( a , n ) = 1 \gcd (a,n)=1 gcd(a,n)=1 ,都有 a n − 1 ≡ 1 ( m o d n ) a^{n-1} \equiv 1 \pmod n an11(modn) 成立,则称这样的数为 卡迈克尔数,卡迈克尔数 有无穷多个,最小的 卡迈克尔数 是 561 561 561,这种数的存在也佐证了费马小定理的逆命题不成立。

  不过 卡迈克尔数 的出现密度较低,在一亿以内的自然数中也仅仅只有 255 255 255 个。


二次探测定理

Strong probable primes


  若 p p p 为质数,则 x 2 ≡ 1 ( m o d p ) x^2 \equiv 1 \pmod p x21(modp) 的解为 x ≡ ± 1 ( m o d p ) x \equiv\pm 1 \pmod p x±1(modp)

  但这个特性并非质数的精确表征,因此通过该式测试的数 p p p,被称为 强可能质数,而通过该式测试的合数则被称为 强伪质数。


非平凡平方根


  截止至最后一次更新,搜索引擎上很多有关 米勒-拉宾 测试都未对该点讲解,所以这里简单介绍一下。

  若 x ≢ ± a ( m o d p ) x \not\equiv\pm\sqrt a\pmod p x±a (modp),满足 x 2 ≡ a ( m o d p ) x^2 \equiv a\pmod p x2a(modp),则称 x x x 是以 p p p 为模的 a a a 的 非 平凡\显然 平方根。

  特别地,当 p p p 为质数时, a a a 不存在非平凡平方根。


Miller-Rabin 素性测试


  米勒-拉宾 素性测试就是将:费马小定理、二次探测定理、非显然平方根的性质结合起来。

  具体地说:

  给定一个整数 n n n,我们先判断它是否为偶数,若是则判断是它是否为 a a a,不是则存在一个 b b b n = b × 2 k + 1 n = b ×2^k + 1 n=b×2k+1 k k k 尽可能的大,此时随机一个 a a a,根据费马小定理,若 n n n 为质数,则存在一个 k ′ < k k' < k k<k,使得 a b ⋅ 2 k ′ ≡ ± 1 ( m o d n ) a^{b\cdot2^{k'}} \equiv \pm 1\pmod n ab2k±1(modn) 成立,否则 a a a 存在一个模 n n n 下的非显然平方根,此时 n n n 必为合数。

  一次 米勒-拉宾 素性测试的误判在 1 4 \frac 14 41,因此建议对于每个数的测试次数都在 8 8 8 次以上。

  关于上述概率的证明,等我实在是太闲的时候补。


bool is_prime(int n) {
    
    
    if ((n & 1) == 0) return n == 2;
    int b = n - 1, k = 0, j;
    while (b & 1 == 0) b >>= 1, ++k;
    for (int i = 0; i < test_time; ++i) {
    
    
        int a = rand() % (n - 1) + 1;
        int v = mod_pow(a, b, n);
        if (v == 1) continue;
        for (j = 0; j < k; ++j) {
    
    
            if (v == n - 1) break;
            v = v * v % n;
        }
        if (j == b) return 0;
    }
    return 1;
}

查找因数


  在程序设计竞赛中,算术基本定理表明,我们可以将任意一个大于 1 1 1 自然数表示为若干个质数的乘积,即 N = p 1 a 1 ⋅ p 2 a 2 ⋅ p 3 a 3 ⋅ ⋯ ⋅ p s s 1 = ∏ i = 1 s p i a i N=p^{a_1}_1\cdot p^{a_2}_2\cdot p^{a_3}_3\cdot \cdots \cdot p^{s_1}_s =\prod_{i=1}^sp^{a_i}_i N=p1a1p2a2p3a3pss1=i=1spiai

  这是很多带有数论标签的题目的突破口,

  因此,掌握如何快速分解一个整数在程序设计竞赛中尤为的重要。


还是试除法


  既然试除法可以通过 1 ∼ N 1 \sim \sqrt N 1N 之间是否存在 N N N 的因数来判断 N N N 是否为质数,那么同样的,

  在试除法的基础上稍作修改,就能计算出 N N N 的所有质因子。

std::map<int, int> factor(int N) {
    
    
    std::map<int, int> factors;
    for (int p = 2; p <= sqrt(N); ++p)
        if (N % p == 0) {
    
    
            int k = 0;
            while (N % p == 0) N /= p, ++k;
            factors[p] += k;
        }
    if (N != 1) ++factors[N];
    return factors;
}

  朴素的试除法复杂度在 O ( N ) O(\sqrt N) O(N ),若在有质数表的情况下试除则复杂度降至 O ( 2 N log ⁡ N ) O(\cfrac{2\sqrt N}{\log N}) O(logN2N )

  关于 质数打表 点这里


生日悖论


  Pollard’s Rho 分解质因数的期望效率是反直觉的,因此在介绍 波拉德的 ρ \rho ρ 之前需要先引入生日悖论。

  原命题我是在《费马大定理》上看到的,但现在书不在手边,就在网上随便搜了一个:

  在一场英式足球赛里,通常会有 23 个人在赛场上:两支参赛队伍各有 11 名球员,外加 1 名裁判。在这23 人里,2 人或 2 人以上具有相同生日的概率是多少?

  答案是约为 50 50 50%,这对于大多数没有接触过概率论的人来说都是反直觉的。

  针对没有接触过概率的读者,这里简单验证一下:

  设 A A A 为事件 n n n 个人生日不同, P ( A ) P(A) P(A) 为事件 A A A 发生的概率,则 A A A 的逆命题 A ˉ \bar A Aˉ n n n 个人中至少有两个人生日相同的概率为 P ( A ˉ ) = 1 − P ( A ) P(\bar A) = 1 - P(A) P(Aˉ)=1P(A) P ( A ) = 365 365 × 365 − 1 365 × 365 − 2 365 × ⋯ × 365 − n + 1 365 = ∏ i = 0 n − 1 365 − i 365 P(A) = \frac{365}{365} × \frac{365 - 1}{365} × \frac{365 - 2}{365} × \cdots × \frac{365 - n + 1}{365} = \prod_{i=0}^{n-1}\frac{365-i}{365} P(A)=365365×3653651×3653652××365365n+1=i=0n1365365i  该式的意义为,

   1 1 1 个人时生日不同的概率显然为 100 100 100%,而第 2 2 2 个人的开始,为了与前面的人生日不同,只能从前面的人生日的其他日期任选一个,于是第 2 2 2 个人有 364 365 \frac{364}{365} 365364 的概率与前面所有人的生日不同,第 3 3 3 人为 363 365 \frac{363}{365} 365363 ⋯ \cdots ,最终这个事件的总概率为它们的乘积。

  借助计算机,我们可以得知,当 n = 23 n = 23 n=23 时, P ( A ˉ ) = 1 − P ( A ) ≃ 0.5 P(\bar A) = 1 - P(A) \simeq 0.5 P(Aˉ)=1P(A)0.5,当 n = 57 n = 57 n=57 P ( A ˉ ) ≃ 0.99 P(\bar A) \simeq 0.99 P(Aˉ)0.99


伪随机数列


  生日悖论启示我们,某个长度为 n n n,分布在为 [ 1 , N ] [1,N] [1,N] 的随机数列 x 1 , x 2 , x 3 , ⋯   , x n x_1, x_2, x_3, \cdots , x_n x1,x2,x3,,xn,它们中最少出现两个相等数字的期望长度不会很大(实际为 π N 2 \sqrt{\cfrac{\pi N}2} 2πN )。

  设 N N N 的某个因子为 p p p,若我们不断的在 [ 1 , N ) [1,N) [1,N) 之间生成随机数 x i x_i xi,同时构造一个随机数列 { y i ∣ y i = x i m o d    p } \{y_i \mid y_i = x_i \mod p\} { yiyi=ximodp},那么当存在一对 i i i j j j,使得 x i ≠ x j x_i \neq x_j xi=xj y i = y j y_i = y_j yi=yj 同时成立,另 d = y i − y j d = y_i - y_j d=yiyj,显然有 d ≡ 0 ( m o d p ) d \equiv 0\pmod p d0(modp) 1 < gcd ⁡ ( d , N ) < N 1 < \gcd(d,N) <N 1<gcd(d,N)<N,此时 ∣ d ∣ \mid d\mid d N N N 的一个因子。

  考虑最坏情况,即 N = p 2 N = p^2 N=p2 p p p 为一个质数( N N N 本身就是质数的情况可以先用 Miller-Rabin 素性测试特判一下),此时 p = N p = \sqrt N p=N { y i } \{y_i\} { yi} 的期望长度为 π N 2 ≃ N 1 4 \sqrt{\cfrac{\pi \sqrt N}2} \simeq N^{\frac 14} 2πN N41


Pollard’s Rho


  光整合上述知识,想要快速的将一个较大整数 N N N 分解为算术基本定理形式也是不够的,光枚举 d d d 的复杂度就已经到了 O ( N ) O(\sqrt N) O(N ),更别提对于每一次枚举还要做一次 gcd ⁡ \gcd gcd

  所以 Pollard 选择一种特殊的伪随机数生成器, x i = x i − 1 2 + c ( m o d N ) x_i = x_{i-1}^2 + c \pmod N xi=xi12+c(modN),其中 c c c 为某个固定常数, x 1 x_1 x1 随机取得。

  例如当 N = 41 , x 1 = 31 , c = 5 N = 41,x_1 = 31,c=5 N=41,x1=31,c=5 时,生成的随机数列为: 31 , 23 , 1 , 6 , 0 , 5 , 30 ˙ , 3 , 14 , 37 , 21 , 36 ˙ , 30 , ⋯ 31,23, 1, 6, 0, 5, \dot{30}, 3, 14, 37, 21, \dot{36}, 30,\cdots 31,23,1,6,0,5,30˙,3,14,37,21,36˙,30,  将数列按下图所示方式排列,会发现性质酷似一个 ρ \rho ρ,算法也因此得名。
请添加图片描述
  (图片摘自 wiki)

  主流的优化方式有 Floyd 判环法,倍增法,这里仅对 判环法 做出阐述和实现。

  若 { x i } \{x_i\} { xi} 上存在一对 i i i j j j 使得 x i − x j ≡ 0 ( m o d p ) x_i - x_j \equiv 0\pmod p xixj0(modp),则有

   x i − x j ≡ x i − 1 2 − x j − 1 2 ≡ ( x i − 1 + x j − 1 ) ( x i − 1 − x j − 1 ) ≡ 0 ( m o d p ) x_i - x_j \equiv x_{i-1}^2 - x_{j-1}^2\equiv (x_{i-1} + x_{j-1})(x_{i-1} - x_{j-1})\equiv 0\pmod p xixjxi12xj12(xi1+xj1)(xi1xj1)0(modp)

  该式表明了,对于所有 k , g k,g k,g k − g = i − j k-g=i-j kg=ij,都有 x k − x g ≡ 0 ( m o d p ) x_k - x_g\equiv 0\pmod p xkxg0(modp),因此使用 Floyd 判环法,依次枚举环上距离为 1 ∼ n 1 \sim n 1n 的随机数对,期望枚举次数为 n = N 1 4 n=N^\frac 14 n=N41

  整个算法的期望复杂度为 O ( N 1 4 log ⁡ N ) O(N^\frac 14\log N) O(N41logN)

int f(int x, int c) {
    
     return x * x + c; }

int gcd(int a, int b) {
    
     return b ? gcd(b, a % b) : a; }

int pollard_rho(int N, int c) {
    
    
    int xi = rand() % (N - 1) + 1, xj = f(xi, c) % N;
    while (xi != xj) {
    
    
        int d = gcd(N, xi - xj);
        if (d > 1) return d;
        xj = f(f(xj, c), c) % N;
        xi = f(xi, c) % N;
    }
    return N;
}

分解质因数


  综合上述所有知识点,我们可以确定出一个算法,可以在较为优秀的复杂度内完成对一个大整数的分解。

  具体地说,对于一个整数 N N N,我们先对其进行素性测试,若是则将其加入质因数集合,否则使用 Pollard’s Rho 找到他的一个非平凡因子 d d d,将 d d d N / d N / d N/d 代回到第一步。

#include <stdio.h>
#include <math.h>
#include <map>

long long multi(long long a, long long b, long long p) {
    
    
    long long res = 0;
    while (b) {
    
    
        if (b & 1) res = (res + a) % p;
        a = (a + a) % p;
        b >>= 1;
    }
    return res;
}

long long qpow(long long a, long long b, long long p) {
    
    
    long long res = 1;
    while (b) {
    
    
        if (b & 1) res = multi(res, a, p);
        a = multi(a, a, p);
        b >>= 1;
    }
    return res;
}

long long gcd(long long a, long long b) {
    
     return b ? gcd(b, a % b) : a; }

int test_time = 8;

bool miller_rabin(long long n) {
    
    
    if (n < 3 || n % 2 == 0) return n == 2;
    long long b = n - 1, k = 0, j;
    while (b % 2 == 1) b /= 2, ++k;
    for (int i = 0; i < test_time; ++i) {
    
    
        long long a = qpow(rand() % (n - 2) + 2, b, n);
        if (a == 1) continue;
        for (j = 0; j < k; ++j) {
    
    
            if (a != n - 1) break;
            a = multi(a, a, n);
        }
        if (j == k) return 0;
    }
    return 1;
}

long long f(long long x, long long c, long long p) {
    
     return (multi(x, x, p) + c) % p; }

long long pollard_rho(long long N, long long c) {
    
    
    long long xi = rand() % (N - 1) + 1, xj = f(xi, c, N);
    while (xi != xj) {
    
    
        long long d = gcd(xi - xj, N);
        if (d > 1) return d;
        xj = f(f(xj, c, N), c, N);
        xi = f(xi, c, N);
    }
    return N;
}

void factor(long long N, std::map<long long, int> &factors) {
    
    
    if (miller_rabin(N)) ++factors[N];
    else {
    
    
        long long c = rand() % (N - 1) + 1;
        long long d = N;
        while (d >= N)
            d = pollard_rho(N, c--);
        factor(N / d, factors);
        factor(d, factors);
    }
}

int main(){
    
    
    long long N;
    while (~scanf("%lld", &N)) {
    
    
        std::map<long long, int> factors;
        printf("%lld=", N);
        factor(N, factors);
        for (std::map<long long, int>::iterator it = factors.begin(); it != factors.end();) {
    
    
            printf("%d^%d", it->first, it->second);
            if (++it != factors.end()) printf("*");
        }
        printf("\n");
    }
}

猜你喜欢

转载自blog.csdn.net/qq_43449564/article/details/123979433