大整数质因数分解
Miller–Rabin 负责快速判断一个大整数是否为质数;
Pollard–Rho 负责从大合数中随机找到一个非平凡因子。
1 从试除法到随机分解
试除法分解一个整数 n n n 需要枚举到 n \sqrt n n 。
当 n n n 接近 10 18 10^{18} 1 0 18 时,n \sqrt n n 接近 10 9 10^9 1 0 9 ,单次试除已经难以承受。此时需要把“逐个寻找因子”改成:
本章主线:安全模乘 → Miller–Rabin 素性测试 → Pollard–Rho 寻找非平凡因子 → 递归分解。
两部分算法承担不同任务:
Miller–Rabin 只能判断当前整数是否为质数;
Pollard–Rho 只能尝试找到当前合数的一个非平凡因子;
递归过程负责继续拆分因子,直到所有部分都成为质数。
2 大整数模乘
2.1 为什么普通乘法会溢出
若 a , b , n a,b,n a , b , n 都接近 long long 上限,即使最终只需要
a b m o d n , ab\bmod n,
ab mod n ,
中间的 a b ab ab 也可能远远超过 long long 的表示范围。
在支持 __int128_t 的 C++ 环境中,可以先将乘积提升到 128 位,再对模数取模。
1 2 3 4 5 6 7 8 9 10 ll mul ( ll a, ll b, ll mod ) { return (ll)( (i128)a * b % mod ); }ll qpow ( ll a, ll k, ll mod ) { ll res = 1 % mod; a %= mod; for ( ; k; k >>= 1 , a = mul ( a, a, mod ) ) if ( k & 1 ) res = mul ( res, a, mod ); return res; }
这里的 mul 是 Miller–Rabin 与 Pollard–Rho 的共同底座。
3 从 Fermat 测试到 Miller–Rabin
3.1 Fermat 测试的局限
费马小定理说明:若 n n n 是质数,且 gcd ( a , n ) = 1 \gcd(a,n)=1 g cd( a , n ) = 1 ,则
a n − 1 ≡ 1 ( m o d n ) . a^{n-1}\equiv1\pmod n.
a n − 1 ≡ 1 ( mod n ) .
因此,若某个底数 a a a 不满足这一同余式,可以确定 n n n 是合数。
但反过来不成立:某些合数也可能对一个甚至许多个底数通过 Fermat 测试。因此,Fermat 测试只能快速排除一部分合数,不能单独作为可靠的素性判定。
3.2 二次探测
若 p p p 是奇质数,则
x 2 ≡ 1 ( m o d p ) x^2\equiv1\pmod p
x 2 ≡ 1 ( mod p )
只有两个解:
x ≡ 1 ( m o d p ) 或 x ≡ − 1 ( m o d p ) . x\equiv1\pmod p
\qquad\text{或}\qquad
x\equiv-1\pmod p.
x ≡ 1 ( mod p ) 或 x ≡ − 1 ( mod p ) .
这是因为
x 2 − 1 = ( x − 1 ) ( x + 1 ) , x^2-1=(x-1)(x+1),
x 2 − 1 = ( x − 1 ) ( x + 1 ) ,
而质数 p p p 整除乘积时,必然整除其中一个因子。
因此,在不断平方的过程中,若一个数在模 p p p 下最终变成 1 1 1 ,它此前只能从 1 1 1 或 − 1 -1 − 1 平方得到。
3.3 强伪素数测试
对待测奇数 n n n ,将
n − 1 = d ⋅ 2 s n-1=d\cdot2^s
n − 1 = d ⋅ 2 s
写成 d d d 为奇数的形式。
对一个底数 a a a ,先计算
x = a d m o d n . x=a^d\bmod n.
x = a d mod n .
若 n n n 是质数,则必须满足以下情况之一:
x = 1 x=1 x = 1 ;
x = − 1 x=-1 x = − 1 ;
在之后至多 s − 1 s-1 s − 1 次平方中出现 − 1 -1 − 1 。
若这些情况都没有出现,则 n n n 一定是合数。
3.4 固定底数
一般的 Miller–Rabin 是概率算法:随机选择若干底数,合数通过所有测试的概率会快速降低。
在 long long 范围内,可以使用一组固定底数完成确定性判定。当前模板使用前十二个质数:
2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37. 2,3,5,7,11,13,17,19,23,29,31,37.
2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 bool miller_rabin ( ll n ) { if ( n < 2 ) return 0 ; if ( n == 2 || n == 3 ) return 1 ; if ( n % 2 == 0 || n % 3 == 0 ) return 0 ; ll d = n - 1 , s = 0 ; while ( d % 2 == 0 ) d >>= 1 , s ++; static const ll primes[] = { 2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37 }; for ( ll p : primes ) { if ( n == p ) return 1 ; ll x = qpow ( p, d, n ); if ( x == 1 || x == n - 1 ) continue ; bool ok = 0 ; for ( int i = 0 ; i < s - 1 ; ++ i ) { x = mul ( x, x, n ); if ( x == n - 1 ) { ok = 1 ; break ; } } if ( !ok ) return 0 ; } return 1 ; }
设固定测试底数的数量为常数,单次判定主要进行若干次快速幂,复杂度可以看作 O ( log n ) O(\log n) O ( log n ) 次模乘。
4 Pollard–Rho
4.1 寻找因子的基本想法
设 p p p 是 n n n 的某个未知质因子。
如果构造一个模 n n n 的伪随机序列
x k + 1 = f ( x k ) , x_{k+1}=f(x_k),
x k + 1 = f ( x k ) ,
那么它同时也在模 p p p 意义下形成一个序列。
当两个位置满足
x i ≡ x j ( m o d p ) , x_i\equiv x_j\pmod p,
x i ≡ x j ( mod p ) ,
就有
p ∣ ( x i − x j ) . p\mid(x_i-x_j).
p ∣ ( x i − x j ) .
因此
gcd ( ∣ x i − x j ∣ , n ) \gcd(|x_i-x_j|,n)
g cd( ∣ x i − x j ∣ , n )
可能暴露出 n n n 的一个非平凡因子。
Pollard–Rho 常用的迭代函数为
f ( x ) = x 2 + c ( m o d n ) . f(x)=x^2+c\pmod n.
f ( x ) = x 2 + c ( mod n ) .
由于有限剩余类上的迭代最终一定进入循环,序列形状类似希腊字母 ρ \rho ρ ,算法因此得名。
4.2 为什么能够较快发生碰撞
若只考虑模未知因子 p p p 后的序列,根据生日悖论,随机取值大约经过 O ( p ) O(\sqrt p) O ( p ) 次就可能发生碰撞。
当 p p p 接近 n \sqrt n n 时,这对应约 O ( n 1 / 4 ) O(n^{1/4}) O ( n 1/4 ) 的期望规模。
这只是随机算法的期望表现,不是严格的最坏复杂度保证。
4.3 批量计算最大公因数
逐步计算
gcd ( ∣ x i − x j ∣ , n ) \gcd(|x_i-x_j|,n)
g cd( ∣ x i − x j ∣ , n )
会产生大量 gcd 调用。
模板将一批差值先乘起来:
V = ∏ ∣ x i − x j ∣ ( m o d n ) , V=\prod |x_i-x_j|\pmod n,
V = ∏ ∣ x i − x j ∣ ( mod n ) ,
再计算
gcd ( V , n ) . \gcd(V,n).
g cd( V , n ) .
只要其中某个差值含有非平凡因子,这个乘积也会含有该因子。代码每累计一段后检查一次 gcd,以减少常数。
如果得到的结果恰好是 n n n ,说明这一轮没有分离出有效因子,外层会重新随机选择参数再运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 mt19937_64 rng ( time(0 ) ) ;ll pollard_rho ( ll n ) { if ( n % 2 == 0 ) return 2 ; if ( n % 3 == 0 ) return 3 ; ll x = 0 , y = 0 , c = rng ( ) % ( n - 1 ) + 1 ; ll val = 1 , d = 1 ; for ( int k = 1 ; ; k <<= 1 , y = x, val = 1 ) { for ( int i = 0 ; i < k; ++ i ) { x = ( mul ( x, x, n ) + c ) % n; val = mul ( val, abs ( x - y ), n ); if ( i % 127 == 0 ) { d = gcd ( val, n ); if ( d > 1 ) return d; } } d = gcd ( val, n ); if ( d > 1 ) return d; } }
pollard_rho(n) 的返回值可能是一个非平凡合因子,也可能在失败时返回 n n n ,所以它的结果仍需要继续判断与拆分。
5 递归质因数分解
5.1 递归结构
对当前整数 n n n :
若 n < 2 n<2 n < 2 ,停止;
若 Miller–Rabin 判断 n n n 为质数,记录它;
否则用 Pollard–Rho 找到因子 p p p ;
递归分解 p p p 与 n / p n/p n / p 。
这与算术基本定理完全对应:每次把一个合数拆成更小的因子,最终所有叶子都会成为质数。
5.2 去除重复因子
当前模板的目标是求最大质因子。
找到因子 p p p 后,代码先不断令 n ← n / p n\gets n/p n ← n / p ,一次去除 p p p 的全部重复贡献,再递归处理剩余部分与 p p p 。
这样即使原数中含有较高次幂,也不需要对同一个因子重复递归。
5.3 最大质因子接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ll max_factor;void _find_fac( ll n ) { if ( n <= max_factor || n < 2 ) return ; if ( miller_rabin ( n ) ) { max_factor = max ( max_factor, n ); return ; } ll p = n; while ( p >= n ) p = pollard_rho ( n ); while ( n % p == 0 ) n /= p; _find_fac( n ); _find_fac( p ); }ll get_max_factor ( ll n ) { max_factor = 0 ; _find_fac( n ); return max_factor; }
其中剪枝条件 n <= max_factor 表示:即使当前 n n n 本身是质数,也不可能更新已经得到的最大质因子。
当前接口只返回最大质因子。如果题目要求完整质因数集合或质因数指数,需要在质数递归终点记录因子,再对结果排序和计数。
6 完整模板
下面汇总当前模板中的全部实现。代码保留原有命名与接口,不再增加额外封装。
▶
Miller-Rabin 与 Pollard-Rho
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 mt19937_64 rng ( time(0 ) ) ;ll mul ( ll a, ll b, ll mod ) { return (ll)( (i128)a * b % mod ); }ll qpow ( ll a, ll k, ll mod ) { ll res = 1 % mod; a %= mod; for ( ; k; k >>= 1 , a = mul ( a, a, mod ) ) if ( k & 1 ) res = mul ( res, a, mod ); return res; }bool miller_rabin ( ll n ) { if ( n < 2 ) return 0 ; if ( n == 2 || n == 3 ) return 1 ; if ( n % 2 == 0 || n % 3 == 0 ) return 0 ; ll d = n - 1 , s = 0 ; while ( d % 2 == 0 ) d >>= 1 , s ++; static const ll primes[] = { 2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37 }; for ( ll p : primes ) { if ( n == p ) return 1 ; ll x = qpow ( p, d, n ); if ( x == 1 || x == n - 1 ) continue ; bool ok = 0 ; for ( int i = 0 ; i < s - 1 ; ++ i ) { x = mul ( x, x, n ); if ( x == n - 1 ) { ok = 1 ; break ; } } if ( !ok ) return 0 ; } return 1 ; }ll pollard_rho ( ll n ) { if ( n % 2 == 0 ) return 2 ; if ( n % 3 == 0 ) return 3 ; ll x = 0 , y = 0 , c = rng ( ) % ( n - 1 ) + 1 ; ll val = 1 , d = 1 ; for ( int k = 1 ; ; k <<= 1 , y = x, val = 1 ) { for ( int i = 0 ; i < k; ++ i ) { x = ( mul ( x, x, n ) + c ) % n; val = mul ( val, abs ( x - y ), n ); if ( i % 127 == 0 ) { d = gcd ( val, n ); if ( d > 1 ) return d; } } d = gcd ( val, n ); if ( d > 1 ) return d; } } ll max_factor;void _find_fac( ll n ) { if ( n <= max_factor || n < 2 ) return ; if ( miller_rabin ( n ) ) { max_factor = max ( max_factor, n ); return ; } ll p = n; while ( p >= n ) p = pollard_rho ( n ); while ( n % p == 0 ) n /= p; _find_fac( n ); _find_fac( p ); }ll get_max_factor ( ll n ) { max_factor = 0 ; _find_fac( n ); return max_factor; }
7 方法选择
问题规模与目标
方法
单个整数较小
试除法
批量处理 1 1 1 到 N N N
埃氏筛或线性筛
只判断 long long 范围内的大整数是否为质数
Miller–Rabin
分解 long long 范围内的大合数
Miller–Rabin + Pollard–Rho
只求最大质因子
get_max_factor
求完整质因数分解
在递归终点收集所有质因子
需要注意:
mul 依赖 i128,用于避免模乘溢出;
Pollard–Rho 是随机算法,不保证每轮都找到有效因子;
pollard_rho 的结果不一定是质数;
得到一个因子后,必须继续递归分解;
多次调用 get_max_factor 时,每次都会重新清空 max_factor;
当前模板面向正整数分解,0 0 0 、负数与符号需要由题目外围单独处理。
8 与前面内容的联系
算术基本定理保证递归分解最终得到唯一的质因数集合与指数;
快速幂和模乘来自模运算的公共底座;
Miller–Rabin 使用费马小定理与二次探测;
Pollard–Rho 使用最大公因数从伪随机序列的碰撞中提取因子;
ExLucas 在模数较大且难以试除分解时,可以使用 Pollard–Rho 获得质数幂分解。
相关内容: