vector<int> breakdown(int N) { vector<int> result; for (int i = 2; i * i <= N; i++) { if (N % i == 0) { // 如果 i 能够整除 N,说明 i 为 N 的一个质因子。 while (N % i == 0) N /= i; result.push_back(i); } } if (N != 1) { // 说明再经过操作之后 N 留下了一个素数 result.push_back(N); } return result;}
Python
def breakdown(N): result = [] for i in range(2, int(sqrt(N)) + 1): if N % i == 0: # 如果 i 能够整除 N,说明 i 为 N 的一个质因子。 while N % i == 0: N //= i result.append(i) if N != 1: # 说明再经过操作之后 N 留下了一个素数 result.append(N) return result
我们能够证明 result 中的所有元素即为 N 的全体素因数。
证明 result 中即为 N 的全体素因数
首先考察 N 的变化。当循环进行到 i 结束时,由于刚执行结束 while(N % i == 0) N /= i 部分,i 不再整除 N。而且,每次除去一个因子,都能够保证 N 仍整除 N。这两点保证了,当循环进行到 i 开始时,N 是 N 的一个因子,且不被任何小于 i 的整数整除。
其次证明 result 中的元素均为 N 的因子。当循环进行到 i 时,能够在 result 中存入 i 的条件是 N % i == 0,这说明 i 整除 N,且已经说明 N 是 N 的因子,故而有 i 是 N 的因子。当对 i 的循环结束时,若 N 不为一,也会存入 result。此时它根据前文,也必然是 N 的一个因子。
其次证明 result 中均为素数。我们假设存在一个在 result 中的合数 K,则必然存在 i 不超过 K,满足 i 是 K 的一个因子。这样的 K 不可能作为循环中的某个 i 存入 result,因为第一段已经说明,当循环到 K 时,N 不被任何小于 K 的 i 整除。这样的 K 也不可能在循环结束后加入,因为循环退出的条件是 i * i > N,故而已经遍历完了所有不超过 K 的 i,而且据上文所说,这些 i 绝不能整除目前的 N,亦即 K。
最后证明,所有 N 的素因子必然出现在 result 中。不妨假设 p 是 N 的一个素因子,但并没有出现在 result 中。根据上文的讨论,p 不可能是循环中出现过的 i。设 i 是退出循环前最后的 i,则 i 严格小于 p,而退出循环后的 N 不被之前的 i 整除,故而 p 整除 N。所以最后的 N 大于一,则根据前文所述,它必然是素数,则 N 就等于 p,必会在最后加入 result,与假设矛盾。
我们需要实现的算法,能够在迭代过程中快速判断 {xnmodp} 是否已经出现重复。将 f 看成以 Zp 为顶点的有向图上的边,我们实际要实现的是一个判环算法。只是将判等改为了判断 gcd(∣xi−xj∣,N) 是否大于一。
Floyd 判环
假设两个人在赛跑,A 的速度快,B 的速度慢,经过一定时间后,A 一定会和 B 相遇,且相遇时 A 跑过的总距离减去 B 跑过的总距离一定是圈长的倍数。
设 a=f(0),b=f(f(0)),每一次更新 a=f(a),b=f(f(b)),只要检查在更新过程中 a 和 b 是否相等,如果相等了,那么就出现了环。
我们每次令 d=gcd(∣xi−xj∣,N),判断 d 是否满足 1<d<N,若满足则可直接返回 d。如果 d=N,则说明 {xi} 已经形成环,在形成环时就不能再继续操作了,直接返回 N 本身,并且在后续操作里调整随机常数 c,重新分解。
基于 Floyd 判环的 Pollard-Rho 算法
[list2tab]
C++
ll Pollard_Rho(ll N) { if (N == 4) return 2; // 因为一开始跳了两步,所以需要特判一下 4 ll c = rand() % (N - 1) + 1; ll t = f(0, c, N); ll r = f(f(0, c, N), c, N); while (t != r) { ll d = gcd(abs(t - r), N); if (d > 1) return d; t = f(t, c, N); r = f(f(r, c, N), c, N); } return N;}
Python
import randomdef Pollard_Rho(N): if N == 4: return 2 # 因为一开始跳了两步,所以需要特判一下 4 c = random.randint(1, N - 1) t = f(0, c, N) r = f(f(0, c, N), c, N) while t != r: d = gcd(abs(t - r), N) if d > 1: return d t = f(t, c, N) r = f(f(r, c, N), c, N) return N
Brent 判环
实际上,Floyd 判环算法可以有常数上的改进。Brent 判环从 k=1 开始递增 k,在第 k 轮,让 A 等在原地,B 向前移动 2k 步,如果在过程中 B 遇到了 A,则说明已经得到环,否则让 A 瞬移到 B 的位置,然后继续下一轮。
可以证明 3,这样得到环之前需要调用 f 的次数永远不大于 Floyd 判环算法。原论文中的测试表明,Brent 判环需要的平均时间相较于 Floyd 判环减少了 24%。
如果每 k 对计算一次 gcd,则算法复杂度降低到 O(p+k−1plogN),这里,logN 为单次计算 gcd 的开销。注意到 k 和 logN 大致同阶时,可以得到 O(p) 的期望复杂度。具体实现中,大多选取 k=128。
这里提供 Brent 判环且加上倍增优化的 Pollard-Rho 算法实现。
实现
[list2tab]
C++
ll Pollard_Rho(ll x) { ll t = 0; ll c = rand() % (x - 1) + 1; ll s = t; int step = 0, goal = 1; ll val = 1; for (goal = 1;; goal <<= 1, s = t, val = 1) { for (step = 1; step <= goal; ++step) { t = f(t, c, x); val = val * abs(t - s) % x; // 如果 val 为 0,退出重新分解 if (!val) return x; if (step % 127 == 0) { ll d = gcd(val, x); if (d > 1) return d; } } ll d = gcd(val, x); if (d > 1) return d; }}
Python
from random import randintfrom math import gcddef Pollard_Rho(x): c = randint(1, x - 1) s = t = f(0, c, x) goal = val = 1 while True: for step in range(1, goal + 1): t = f(t, c, x) val = val * abs(t - s) % x if val == 0: return x # 如果 val 为 0,退出重新分解 if step % 127 == 0: d = gcd(val, x) if d > 1: return d d = gcd(val, x) if d > 1: return d s = t goal <<= 1 val = 1
复杂度
Pollard-Rho 算法中的期望迭代次数为 O(p),这里 p 是 N 的最小素因子。具体实现无论是采用 Floyd 判环还是 Brent 判环,如果不使用倍增优化,期望复杂度都是 O(plogN);在加上倍增优化后,可以近似得到 O(p) 的期望复杂度。