Pollard Rho 大数因子分解
极难4Pollard Rho 大数因子分解——用随机游走和生日悖论快速拆解大数
你有没有想过,当遇到一个像 123456789012345678901 这样的大数时,怎么快速知道它的质因数?用试除法一个一个试?那可能要试到天荒地老。Pollard Rho 算法就是专门用来对付这种难题的“拆弹专家”——它能用随机游走的办法,像两条蛇在数字迷宫中碰撞一样,快速找到大数的一个非平凡因子(既不是 1 也不是它本身的因子)。今天我们就来彻底搞懂它。
一、为什么我们需要 Pollard Rho?
在日常生活中,分解一个很小的数(比如 24 = 2×2×2×3)很容易,挨个除一下就行了。但如果数很大(比如几十位十进制数),试除法需要的步骤会爆炸式增长。例如,一个 20 位的合数,如果用试除法,最坏情况要试到 10^10 次——电脑也要算很久。
Pollard Rho 算法采用了一种巧妙思路:不是老老实实地试除,而是利用概率和随机,快速撞大运地找到一个因子。它的期望时间复杂度只有 O(n^{1/4}),对于 60 位以下的十进制数几乎瞬间完成。它是现代密码学、数论计算中不可或缺的工具。
二、核心思想:生日悖论 + 随机游走
1. 生日悖论:为什么随机碰撞这么容易?
先问一个问题:一个班级里至少有多少人,才能让“至少两个人的生日相同”的概率超过 50%?
很多人会猜 183(一年的一半),但实际上只需要 23 人!这是因为我们不是在找两个特定的人,而是任意两个人之间都可能撞上。这种“看似很小的样本空间,却很容易发生碰撞”的现象就叫生日悖论。
换成数学语言:如果样本空间的大小是 M(比如一年有 365 天),那么随机抽取大约 √M 个样本,就有很高的概率出现碰撞。23 ≈ √365,√365 ≈ 19.1,所以 23 人已经够了。
Pollard Rho 利用了这个原理:我们要找的因子 p 是 n 的一个约数。模 p 下的“空间”大小只有 p,所以只需要生成大约 √p 个随机数,就有很大概率在模 p 下出现重复(碰撞)。而一旦出现重复,就代表我们找到了一个跟 p 有关的信息,从而可以求出 p 的值。
2. 随机游走:两只蚂蚁在数字广场
想象一个巨大的数字广场,广场上布满了编号从 0 到 n-1 的格子。我们要找一个“隐藏陷阱”——非平凡因子 p。广场实际上由许多小隔间组成(模 p 的同余类),每个小隔间的大小是 p,但整体 n 很大。
Pollard Rho 算法派出了两只蚂蚁(两个随机函数值),它们按照同一个规则在广场上左跳右跳。这个规则是:
f(x) = (x² + c) mod n
其中 c 是一个随机常数,x 是当前位置。
因为模 n 后总格子数是 n,两个蚂蚁独立随机游走,在 n 个格子中相遇的概率很低。但是!如果 n 有一个因子 p,那么我们在模 p 的意义下看,实际上只有 p 个格子(因为模 p 会把很多不同位置的蚂蚁归到同一个“小隔间”)。p 比 n 小很多,所以两只蚂蚁在模 p 的小隔间里很容易相遇——就像在 365 个生日里找重复一样。
举个生活中的例子:你和朋友分别从不同的入口进入一个大型游乐场(有很多区域),你们想碰面。游乐场有 1000 个地址编号(对应模 n 下的不同值)。但游乐场被一条小河分成了左右两半(相当于两个 p 区间),你俩只要落在同一半区域内就算“碰撞”。因为每半区域只有 500 个地址,你们随机游走,碰上的几率大大增加。Pollard Rho 就是用这个原理,找到那个“小河”的边界——也就是因子 p。
三、数学原理与算法流程
1. ρ 形循环
从初始值 x₀ 开始,不断应用 f(x) = (x² + c) mod n,会得到一个序列:
x₀ → x₁ → x₂ → ...
由于模 n 只有 n 个可能值,这个序列最终一定会进入循环。因为序列的形状像希腊字母 ρ(先一条直线,然后绕圈),所以算法叫做 Rho(ρ)。
同时,考虑模 p 下的序列(p 是 n 的某个因子),由于模 p 只有 p 个可能值,它会更快地进入循环。也就是说,在模 p 下,两个不同的位置 xᵢ 和 xⱼ(i<j)可能相等,但在模 n 下它们不一定相等。此时,|xᵢ - xⱼ| 就是 p 的倍数,但不是 n 的倍数(因为模 n 下不相等),所以 gcd(|xᵢ - xⱼ|, n) 就会是 p 或 p 的倍数——这正是我们要找的非平凡因子。
2. Floyd 判圈法:快慢指针
如何高效找到这个碰撞?Pollard Rho 使用了 Floyd 判圈法(也叫龟兔赛跑算法):
- 设置两个指针:慢指针(slow)每次走一步,快指针(fast)每次走两步。
- 初始时,slow = x₀,fast = x₀。
- 每次迭代:slow = f(slow),fast = f(f(fast))(即 fast 走两步)。
- 计算 d = gcd(|slow - fast|, n)。
- 如果 d > 1 且 d < n,则找到了一个非平凡因子 d。
- 如果 d == n,说明可能由于随机常数 c 选择不当导致两个指针在模 n 下完全同步(即循环全等),此时更换 c 重新尝试。
- 如果一直没有找到,继续迭代直到 slow == fast(在模 n 下进入循环),此时说明当前 c 不合适,重新开始。
为什么快慢指针有效?
想象你和一个朋友在操场上跑步,你慢跑,他快跑(速度是你两倍)。如果操场是圆形,他最终会追上你——这就表示出现了碰撞。在模 p 的“小操场”上,由于 p 很小,快慢指针很快就能相遇,从而暴露因子 p。
3. 优化技巧
直接每次迭代都计算 gcd 是很慢的,因为 gcd 涉及大数取模运算。常见的优化是 批量计算:累积一段步数(比如 128 步)的差值乘积,再统一求 gcd。如果 gcd 不为 1,再回头细查是哪一步产生的。这样能大幅减少 gcd 调用次数。
此外,在进入 Pollard Rho 之前,一般会先用试除法去除小因子(比如 2, 3, 5, 7 等),让 n 变得相对小一些,提高效率。
四、完整的分解决策
Pollard Rho 通常与 Miller-Rabin 素性测试 配合使用,形成一套完整的质因数分解流程:
- 输入一个大于 1 的整数 n。
- 试除小因子(可选):先检查 2, 3, 5, 7, 11 等小质数,快速剥离小因子。
- 判断 n 是否为质数:用 Miller-Rabin 测试。如果是质数,则直接输出 n。
- 用 Pollard Rho 找一个非平凡因子 d:
- 如果 d == n,则更换参数重试。
- 否则,递归分解 d 和 n/d。
- 重复直到所有因子都是质数。
这样就能得到完整的质因数分解,并且每个因子都是质数。
五、代码实现
下面是 C++ 和 Python 的完整代码,配合 Miller-Rabin 和 Pollard Rho,能够分解任意大整数(注意大整数乘法溢出问题,C++ 中用 __int128,Python 自动支持大整数)。
C++ 实现
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
// 快速乘(模乘),避免 a*b 溢出
ull mod_mul(ull a, ull b, ull m) {
return (ull)((__int128)a * b % m);
}
// 快速幂取模
ull mod_pow(ull a, ull b, ull m) {
ull res = 1;
a %= m;
while (b) {
if (b & 1) res = mod_mul(res, a, m);
a = mod_mul(a, a, m);
b >>= 1;
}
return res;
}
// Miller-Rabin 确定性测试(针对 64 位整数)
bool miller_rabin(ull n) {
if (n < 2) return false;
if (n == 2 || n == 3) return true;
if (n % 2 == 0) return false;
ull d = n - 1;
int s = 0;
while (d % 2 == 0) { d /= 2; s++; }
// 用前 12 个质数作为底数,对 64 位整数足够可靠
ull bases[] = {2,3,5,7,11,13,17,19,23,29,31,37};
for (ull a : bases) {
if (a >= n) continue;
ull x = mod_pow(a, d, n);
if (x == 1 || x == n-1) continue;
bool composite = true;
for (int i = 0; i < s-1; i++) {
x = mod_mul(x, x, n);
if (x == n-1) { composite = false; break; }
}
if (composite) return false;
}
return true;
}
// 伪随机函数 f(x) = (x^2 + c) % n
ull f(ull x, ull c, ull n) {
return (mod_mul(x, x, n) + c) % n;
}
// 求最大公因数
ull gcd(ull a, ull b) {
while (b) {
ull t = a % b;
a = b;
b = t;
}
return a;
}
// Pollard Rho 寻找一个非平凡因子
ull pollard_rho(ull n) {
if (n % 2 == 0) return 2; // 先检查小因子
if (n % 3 == 0) return 3;
// 随机种子(实际中最好用更好的随机数发生器)
srand(time(0));
ull c = rand() % (n-1) + 1; // 随机常数 c,范围 [1, n-1]
ull x = rand() % (n-2) + 2; // 随机起点,范围 [2, n-1]
ull y = x; // 快指针初始值
ull d = 1;
// Floyd 判圈
while (d == 1) {
x = f(x, c, n); // 慢指针走一步
y = f(f(y, c, n), c, n); // 快指针走两步
d = gcd(x > y ? x - y : y - x, n); // 计算差值绝对值与 n 的 gcd
}
// 如果 d==n,说明因 c 不合适导致循环全等,重新尝试
if (d == n) return pollard_rho(n);
return d;
}
// 递归分解质因数
void factor(ull n, vector<ull>& factors) {
if (n == 1) return;
if (miller_rabin(n)) {
factors.push_back(n);
return;
}
ull d = pollard_rho(n);
factor(d, factors);
factor(n / d, factors);
}
int main() {
ull num;
cout << "请输入一个大于1的整数: ";
cin >> num;
if (num <= 1) {
cout << "输入必须大于1" << endl;
return 0;
}
vector<ull> factors;
factor(num, factors);
sort(factors.begin(), factors.end());
cout << num << " 的质因数分解: ";
for (size_t i = 0; i < factors.size(); i++) {
cout << factors[i];
if (i != factors.size() - 1) cout << " * ";
}
cout << endl;
return 0;
}
Python 实现
import random
import math
def mod_mul(a, b, m):
"""快速乘(Python 直接乘法即可,因为自动大数)"""
return (a * b) % m
def mod_pow(a, b, m):
"""快速幂取模"""
res = 1
a %= m
while b:
if b & 1:
res = mod_mul(res, a, m)
a = mod_mul(a, a, m)
b >>= 1
return res
def miller_rabin(n, k=12):
"""Miller-Rabin 素性测试,k 为测试次数"""
if n < 2:
return False
small_primes = [2,3,5,7,11,13,17,19,23,29,31,37]
for p in small_primes:
if n % p == 0:
return n == p
d = n - 1
s = 0
while d % 2 == 0:
d //= 2
s += 1
for _ in range(k):
a = random.randrange(2, n - 1)
x = mod_pow(a, d, n)
if x == 1 or x == n - 1:
continue
composite = True
for _ in range(s - 1):
x = mod_mul(x, x, n)
if x == n - 1:
composite = False
break
if composite:
return False
return True
def gcd(a, b):
"""辗转相除法求最大公因数"""
while b:
a, b = b, a % b
return a
def pollard_rho(n):
"""Pollard Rho 找非平凡因子"""
if n % 2 == 0:
return 2
if n % 3 == 0:
return 3
# 随机参数
c = random.randrange(1, n - 1) # 随机常数 c
x = random.randrange(2, n - 1) # 随机起点
y = x
d = 1
# Floyd 判圈
while d == 1:
x = (mod_mul(x, x, n) + c) % n # 慢指针走一步
y = (mod_mul(y, y, n) + c) % n
y = (mod_mul(y, y, n) + c) % n # 快指针走两步
d = gcd(abs(x - y), n)
if d == n: # 失败,重试(更换随机参数)
return pollard_rho(n)
return d
def factor(n, factors=None):
"""递归分解 n,结果存入 factors 列表"""
if factors is None:
factors = []
if n == 1:
return factors
if miller_rabin(n):
factors.append(n)
return factors
d = pollard_rho(n)
factor(d, factors)
factor(n // d, factors)
return factors
def main():
try:
num = int(input("请输入一个大于1的整数: "))
if num <= 1:
print("输入必须大于1")
return
factors = factor(num, [])
factors.sort()
print(f"{num} 的质因数分解: {' * '.join(map(str, factors))}")
except ValueError:
print("请输入有效整数")
if __name__ == "__main__":
main()
六、常见错误与注意事项
1. 没有先判断 n 是否为质数
如果直接对一个大质数运行 Pollard Rho,它可能会陷入死循环(因为质数没有非平凡因子)。所以必须先做 Miller-Rabin 素性检测,只有当确定 n 是合数时才去分解。
2. 随机常数 c 选择不当导致无限循环
如果 c 选择不好,快慢指针可能在模 n 下直接进入一个循环且永远不产生有效 d。代码中的实现会在 d == n 时重新尝试,但如果你忘了加重试逻辑,就可能无限循环。因此一定要在检测到 d == n 时更换 c 并重新开始。
3. 大数乘法溢出(C++)
C++ 中用 unsigned long long 最大约 1.8e19,两个这么大的数相乘会直接溢出。必须使用 __int128(GCC 扩展)或者手动实现快速乘(如用 __int128 乘完再取模)。Python 则无此烦恼。
4. 没有试除小因子
如果 n 含有很小的质因子(如 2、3、5),直接跑 Pollard Rho 也能找到,但效率较低。通常先试除 2、3、5、7、11 等前几个质数,把“软柿子”先捏掉,剩下的硬骨头再交给 Pollard Rho。
5. 随机数种子
在 C++ 中,如果 srand(time(0)) 在很短的时间内多次调用,种子可能相同,导致每次运行得到相同随机数。对于竞赛或生产环境,建议使用更好的随机数生成器(如 std::mt19937)。
七、性能与局限
- 时间复杂度:期望 O(n^{1/4}),对于 60 位十进制数(约 200 位二进制)能在秒级内分解。
- 空间复杂度:O(1),仅使用常数个变量。
- 局限性:
- 对于本身就是质数的数,必须先通过 Miller-Rabin 排除,否则算法无法找到因子。
- 对于某些特殊的合数(如完全平方数、两个因子相差很小的数),算法依然高效。
- 对于超过 1000 位的超大合数,Pollard Rho 会变得很慢,这时需要使用更高级的算法(如二次筛法 QS、数域筛法 NFS)。
八、练习
- 挑战 RSA-100:查找 RSA-100(一个 100 位十进制的合数),用你的程序尝试分解它。看看需要多少时间?如果太慢,可以尝试优化批量 gcd。
- 输出指数形式:修改代码,使得分解结果以指数形式输出,例如
2^3 * 3^2 * 5。 - 思考题:
- 为什么 Pollard Rho 要用平方加常数的伪随机函数?如果改用线性函数
f(x) = (a*x + c) mod n会怎样?提示:线性函数生成的序列很容易预测,碰撞模式过于规整。 - 如果 n 是质数的幂,例如 n = p^k,Pollard Rho 能否正确分解?它会不会返回 p 作为因子?
- 为什么 Pollard Rho 要用平方加常数的伪随机函数?如果改用线性函数
九、相关知识点指引
- Miller-Rabin 素性测试:快速判断一个数是质数还是合数,是 Pollard Rho 的前置步骤。
- 试除法:最朴素的因数分解方法,适合小整数。
- 费马小定理与二次探测定理:Miller-Rabin 的理论基础。
- Floyd 判圈算法:用于检测链表或序列中的循环,在 Pollard Rho 中用来检测模 p 下的碰撞。
- 生日悖论:概率论中的一个反直觉现象,也是 Pollard Rho 高效性的理论保证。
- 二次筛法 (QS) 和 数域筛法 (NFS):分解超大型整数(如 100 位以上)的进阶算法,常被用于破解 RSA 密码。
希望这篇文章能帮你彻底理解 Pollard Rho 算法的原理和实现。下次遇到大数分解的问题,你就可以胸有成竹地写出代码了!
例题精讲
Pollard Rho算法中,用于检测循环的经典技术是什么?
Pollard Rho算法能够保证在多项式时间内找到一个大整数的一个非平凡因子。
以下是用Python实现的Pollard Rho算法核心循环片段,请填充缺失的条件判断:
import math
def pollard_rho(n):
c = 1
f = lambda x: (x*x + c) % n
x = 2
y = 2
d = 1
while d == 1:
x = f(x)
y = f(f(y))
d = math.gcd(abs(x-y), n)
if ___:
return d
return n在使用Pollard Rho算法分解大整数之前,通常先执行哪一步来优化效率?
Pollard Rho算法分解n的期望时间复杂度为O(n^(1/4))。