「总结」Pollard Rho

\[\newcommand\abs[1]{\lvert#1\rvert} \]

题目链接

前置知识:生日悖论

\([1,n]\) 中随机选取多少个数,才有至少 \(\frac{1}{2}\) 的概率选出两个相同的数?(这里只需要求一个上界)。


引理:\(\forall x\in\mathbb{R}\)\(1+x\le e^x\)

证明:设 \(f(x)=e^x\),则 \(f(x)\)\((0,f(0))\) 的切线为 \(y=x+1\)。又因为 \(f(x)\) 是下凸函数,所以 \(f(x)\) 的图像一定在直线 \(y=x+1\) 的上方。


设答案为 \(k\)。正难则反,我们计算这些数两两不同的概率:

\[\prod_{i=0}^{k-1}\frac{n-i}{n}\le\frac{1}{2} \]

由引理,\(\frac{n-i}{n}=1+(-\frac{i}{n})\le e^{-\frac{i}{n}}\)。因此 \(k\) 只需要满足:

\[\prod_{i=0}^{k-1}e^{-\frac{i}{n}}=e^{-\frac{k(k-1)}{2n}}\le\frac{1}{2} \]

\(\frac{k(k-1)}{2n}\ge\ln 2\)。因此令 \(k=1+\left\lceil\sqrt{2\ln 2\times n}\right\rceil\) 即可。

题目分析

Pollard Rho 算法能以期望 \(O(n^{1/4})\) 的时间复杂度求出合数 \(n\) 的一个非平凡因子。注意 \(n\) 必须是合数,因此该算法必须配合 Miller Rabin 素性测试使用。

算法希望通过求最大公约数找到 \(n\) 的一个非平凡因子。因为 \(\forall k\in \mathbb{Z}^+\),有 \(\gcd(n,k)\mid n\),所以只要 \(1<\gcd(n,k)<n\),就能找到 \(n\) 的一个非平凡因子。而满足条件的 \(k\) 是很多的,\(n\) 的任意质因子 \(p\) 的任意一个倍数都有很大概率满足条件(不满足条件的情况只有 \(n\mid k\))。

因为求出的 \(\gcd\)\(n\) 的情况很难处理,我们规定算法的返回值是 \(n\) 的一个大于 \(1\) 的因子,但是这个因子可能为 \(n\)。如果需要求 \(n\) 的非平凡因子,则需要重复调用函数,直到返回的因子小于 \(n\)。下文中我们只讨论如何求出 \(n\) 的一个大于 \(1\) 的因子。

我们在 \([0,n)\) 中随机取一个常数 \(c\),定义 \(f(x)=(x^2+c)\bmod n\)。通过这个函数 \(f\),我们能生成一个数列 \(\{x_i\}\),其中 \(x_0=0\)\(x_i=f(x_{i-1})\)。根据生日悖论,数列 \(x\) 期望在 \(O(\sqrt n)\) 的位置就会进入循环节,因此循环节的长度也是 \(O(\sqrt n)\)

一个关键性质:设 \(p\)\(n\) 的任意质因子,若 \(x\equiv y\pmod p\),则 \(f(x)\equiv f(y)\pmod p\)。这是因为给一个数减去 \(n\) 的任意一个倍数不会影响模 \(p\) 的余数。因此,设数列 \(\{y_i\}\) 满足 \(y_i=x_i\bmod p\),则数列 \(y\) 期望在 \(O(\sqrt p)\) 的位置就会进入循环节,且循环节长度为 \(O(\sqrt p)\)。取 \(p\)\(n\) 的最小质因子,则 \(p\le\sqrt n\),因此我们期望在 \(O(n^{1/4})\) 步内就能进入一个长为 \(O(n^{1/4})\) 的(\(y\) 的)循环节。

一旦我们找到两个不同的下标 \(i,j\),使得 \(y_i=y_j\),就有 \(p\mid(x_i-x_j)\),这样 \(\gcd(\abs{x_i-x_j},n)\) 就一定大于 \(1\),也就是说我们找到了一组解。更进一步地,我们发现,只要 \(i,j\) 进入了 \(y\) 的循环节且 \((j-i)\) 是循环节长度的倍数,就有 \(y_i=y_j\)。一个简单的思路是从小到大枚举 \((j-i)\)。但是我们要考虑到因为选的 \(c\) 太差,陷入了 \(x\) 的循环节而无法找到 \(n\) 的大于 \(1\) 的因子的情况。为了判断这种情况,我们有两种实现。

Floyd 判环

初始时令 \(i=j=0\),每次令 \(i\gets i+1\)\(j\gets j+2\)。如果 \(x_i=x_j\),说明已经进入了 \(x\) 的循环节还没有找到 \(n\) 的大于 \(1\) 的因子,直接返回 \(n\),提示调用者需要再跑一遍。否则设 \(d=\gcd(\abs{x_i-x_j},n)\)。如果 \(d\) 大于 \(1\) 就直接返回。否则继续迭代。

这种做法因为需要求多次 \(\gcd\),期望时间复杂度是 \(O(n^{1/4}\log n)\) 的。这仍然不够优秀,我们还有更高效的实现方式。

倍增判环

因为求 \(\gcd\) 的时间复杂度是 \(O(\log n)\) 的,所以对于每一对 \((i,j)\) 都求一次 \(\gcd\) 十分低效。我们可以将若干个 \(\abs{x_i-x_j}\) 乘起来后再求 \(\gcd\),这样如果分别求 \(\gcd\) 能找到一个大于 \(1\) 的因子,乘在一起求也能找到一个大于 \(1\) 的因子,只不过有可能这个因子变成了 \(n\)。不过我们为了时间效率,愿意放弃这种小概率的情况。

倍增判环指的是,假设不考虑 \(i,j\) 都必须进入 \(y\) 的循环节的限制,我们从小到大枚举 \(k\),依次考虑 \(i=0\)\(1\le j\le 2^k\) 的情况。如果存在某个 \(j\) 使得 \(x_i=x_j\),说明我们已经陷入了循环节,直接返回 \(n\)。否则我们将这些 \(\abs{x_i-x_j}\) 乘在一起,再求一遍 \(\gcd\),如果这个 \(\gcd>1\) 就返回。

然而我们考虑到还有 \(i,j\) 必须进入循环节的限制,不妨在考虑完每个 \(k\) 之后,令 \(i\gets j\),下一轮考虑所有 \(i<j\le i+2^k\) 的情况。这样就解决了刚刚的问题。

一个玄学的优化:如果我们乘的项数太多,则 \(\gcd=n\) 的概率会变得很大,这样不够优秀。因此我们可以设置一个常数 \(c\),每隔 \(c\) 个位置求一次 \(\gcd\)。根据理论分析和实践证明,取 \(c=127\) 较为优秀(\(c=127\) 在 OI 范围内已经比求 \(\gcd\) 的复杂度略高了),因此这样做的期望时间复杂度可视为 \(O(n^{1/4})\)

posted @ 2023-11-06 21:23  ClHg2  阅读(63)  评论(0编辑  收藏  举报