CC++ & Algorithm

Pollard Rho 大数因子分解

极难4
语言版本:通用
概述:用随机游走和生日悖论,像两条蛇在数字迷宫中碰撞一样,快速找到大数的一个非平凡因子。

Pollard 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. 输入一个大于 1 的整数 n
  2. 试除小因子(可选):先检查 2, 3, 5, 7, 11 等小质数,快速剥离小因子。
  3. 判断 n 是否为质数:用 Miller-Rabin 测试。如果是质数,则直接输出 n。
  4. 用 Pollard Rho 找一个非平凡因子 d
    • 如果 d == n,则更换参数重试。
    • 否则,递归分解 d 和 n/d。
  5. 重复直到所有因子都是质数

这样就能得到完整的质因数分解,并且每个因子都是质数。

五、代码实现

下面是 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)。

八、练习

  1. 挑战 RSA-100:查找 RSA-100(一个 100 位十进制的合数),用你的程序尝试分解它。看看需要多少时间?如果太慢,可以尝试优化批量 gcd。
  2. 输出指数形式:修改代码,使得分解结果以指数形式输出,例如 2^3 * 3^2 * 5
  3. 思考题
    • 为什么 Pollard Rho 要用平方加常数的伪随机函数?如果改用线性函数 f(x) = (a*x + c) mod n 会怎样?提示:线性函数生成的序列很容易预测,碰撞模式过于规整。
    • 如果 n 是质数的幂,例如 n = p^k,Pollard Rho 能否正确分解?它会不会返回 p 作为因子?

九、相关知识点指引

  • Miller-Rabin 素性测试:快速判断一个数是质数还是合数,是 Pollard Rho 的前置步骤。
  • 试除法:最朴素的因数分解方法,适合小整数。
  • 费马小定理与二次探测定理:Miller-Rabin 的理论基础。
  • Floyd 判圈算法:用于检测链表或序列中的循环,在 Pollard Rho 中用来检测模 p 下的碰撞。
  • 生日悖论:概率论中的一个反直觉现象,也是 Pollard Rho 高效性的理论保证。
  • 二次筛法 (QS)数域筛法 (NFS):分解超大型整数(如 100 位以上)的进阶算法,常被用于破解 RSA 密码。

希望这篇文章能帮你彻底理解 Pollard Rho 算法的原理和实现。下次遇到大数分解的问题,你就可以胸有成竹地写出代码了!

例题精讲

1单选题

Pollard Rho算法中,用于检测循环的经典技术是什么?

A二分法
BFloyd判圈算法
C欧几里得算法
D快速幂
2判断题

Pollard Rho算法能够保证在多项式时间内找到一个大整数的一个非平凡因子。

3填空题
以下是用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
4单选题

在使用Pollard Rho算法分解大整数之前,通常先执行哪一步来优化效率?

A试除法
BMiller-Rabin素性测试
C扩展欧几里得算法
D蒙哥马利模乘
5判断题

Pollard Rho算法分解n的期望时间复杂度为O(n^(1/4))。