Pollard Rho 小记

· · 题解

对于 Pollard Rho 算法流程和卡常技巧其他题解已经讲得很详细了,但其中笔者在学习时遇到以下问题:

  1. 复杂度正确性?

  2. 为何要用那个 Pollard 伪随机数列而非 mt19937 直接随一个?

  3. 怎么想到的这个算法?

这些现有题解并未较完整的解答,本文将更完整的介绍。

通过对这些问题的理解,可以加深读者对于 Pollard Rho 算法正确性的信任,同时可以在背板子的时候明白每步在干什么,避免背了就忘的问题。

问题引入

对于分解质因数,相信大家都会 $O(\sqrt n)$ 的暴力试除法,但质因数个数 $\omega(n)$ 是比 $\log(n)$ 还小的,看起来十分浪费,但直接分解的算法似乎并不显然。 于是我们退而求其次,先考虑解决下面这个问题: $\circ$ 对于一个合数 $n$ , 找到他的一个非平凡因子。 其中非平凡因子是 $n$ 的因子中,除了 $1$ 和 $n$ 的一个因数。这里因数不一定是质因数。 我们考虑用随机的做法,直接随看是不是 $n$ 的因数当 $n$ 只有两个因数时死的透透的。 生日悖论是随机做法中较常利用的一个结论,生日悖论可简单描述为:$m$ 个数中随机选 $k$ 次,当 $k\ge\sqrt m$ 时,选的 $k$ 个数两两不同的概率可忽略。 我们考虑用生日悖论去撞他。 记 $n$ 的最小因子为 $p$ ,有 $p\le \sqrt n

考虑我们随了一个可重集 X=\{x_i\}x_i 的值域为 [0,n-1] (当然,整数),我们记 y_i=(x_i\mod p),于是我们得到了另一个可重集 Y=\{y_i\} 值域是 [0,p-1]

由生日悖论我们知道,当 |Y|>\sqrt p 时,Y 中基本上一定有至少一个数对 (y_i,y_j) 相等,设这样有至少一个数对相等的概率为 P_1 , 生日悖论告诉我们 P_1=1-\epsilon , 即非常接近 1

所以 X 中有同样 P_1 的概率有至少一个数对 (x_i,x_j) ,满足 x_i\equiv x_j\pmod p

考虑 x_i=x_j 的情况,出现这种情况的概率 P_2 是极小的 , 而剩下的 x_i\ne x_j 的情况,有 p|(|x_i-x_j|)

P_2 极小” 这个结论易于理解,即 n 个里选 k 个 ,k\le \sqrt p\le n^{\frac{1}{4}} ,能选出两个相同的数概率很小。具体证明大概挺数数的,读者有兴趣可以自己推一推,不是本文重点。

这说明什么?这说明我们只需要找 k(k\le \sqrt p\le n^{\frac{1}{4}}) 个数 ,里面包含的信息就有 P_1-P_2 的概率包含 p 的一个倍数!

此时之前那对 (x_i,x_j) 满足 \gcd(|x_i-x_j|,n) 一定为 n 的一个因数,且为 p 的一个倍数。

因为 $p|\gcd(|x_i-x_j|,n)$ 且 $p>1$ ,所以 $\gcd(|x_i-x_j|,n)\ne 1

所以 \gcd(|x_i-x_j|,n) 一定是 n 的一个非平凡因子。

由此,我们将问题转化为下面这个形式 :

由之前的证明,可以知道加入次数 $\le n^{\frac{1}{4}}

暴力枚举进行判断是 O((n^{\frac{1}{4}})^2)=O(\sqrt n) 的,跟试除一个复杂度。

这个东西似乎也不好数据结构优化,这就是不能直接用 mt19937 随一个数列的原因。

Pollard 的伪随机数列

直接维护不好做,我们考虑在随机的数列上做文章。

我们需要使随机的数列有一些性质 ,使得只需要通过枚举少数几个 S 中的数即可判断是否存在 b 使得 \gcd(|a-b|,n)\ne 1

Pollard 给出了这样一个数列 :

x_i=(x_{i-1}^2+c) \mod n

我们考虑这个数列有什么好处呢?

对于 $\gcd(|x_i-x_j|,n)\ne 1$ ,记一个 $n$ 的因数 $q|(|x_i-x_j|)$ ,我们考虑 $x_{i+1}$ 和 $x_{j+1}$ ,满足 $|x_{i+1}-x_{j+1}|\equiv|x_i^2+c-(x_j^2+c)|\equiv|(x_i-x_j)\times (x_i+x_j)|\pmod n

而因为 |x_{i+1}-x_{j+1}|\in [0,n-1] , 所以 |x_{i+1}-x_{j+1}|=(|(x_i-x_j)\times (x_i+x_j)|\mod n)

发现仍满足 q|(|x_{i+1}-x_{j+1}|) 但有概率 x_i+x_j=n 而使其变为 0 或者其他情况使得 n|(|(x_i-x_j)\times(x_i+x_j)|) ,这样得到的 \gcd 不再是非平凡因子了,造成非平凡因子的损失。这样情况概率大约 \dfrac{1}{n} 量级,影响并不大,我们一会再讨论。

我们先讨论比较好的情况 ,即所有 (i,j) , 若 \gcd(|x_i-x_j|,n)\ne 1\gcd(|x_{i+t}-x_{j+t}|,n)\ne 1

对于相同距离的点对 ,我们只需要每个距离只判断一次即可。

这样我们就可以有很多办法处理了,比如先加入 k 个,记为 x_1x_k , 然后考虑第 k+1 个位置,我们对所有 i\in[1,k] 查是否有 \gcd(|x_i-x_{k+1}|,n)\ne 1

但由于要考虑到之前说的非平凡因子损失的情况,各种判法有实际表现上的差异。一个常用的是倍增判法:

判断每个 x_ii 以下最大的 2 的整数次幂 j 是否 \gcd(|x_i-x_{j}|,n)\ne 1

这是一个伪随机数列,他足够随机。这个比较玄学,别的题解有所涉及,在本文中只作为结论进行介绍。 但他毕竟是一个伪随机数列,当有数列的两项相等的时候,由于生成下一项的方式一样,这个数列就会出现循环,出现一个 $\rho$ 的形状。 有可能非常不幸随到的 $\rho$ 非常小 ,这时需要进行判环。 我们直接在那个倍增做法上进行改进。 考虑累计 $\prod\limits_{j<i\le2j} |x_i-x_j|\mod n$ ,其中 $j$ 依然是小于 $i$ 的最大 $2$ 的整数次幂,这样对于一轮倍增结束后,判断累积的值是否为 $0$ , 即可知道路径上是否有两值相等,即出现环。有可能因为非平凡因子损失而误判环, 可由于非平凡因子损失概率极低所以实际表现仍是极好的。 还有一些常数优化这里不介绍了。 至此,我们以 $O(\sqrt p)$ 解决了 “对于一个合数 $n$ , 找到他的一个非平凡因子”这个问题,其中 $p$ 是 $n$ 的最小非平凡因子。 我们考虑解决第一个问题,即 $\circ$ 一个数 $n$ , 将他分解质因数。 我们用 Miller Rabin 判断一个数是否是质数,如果是就结束,否则考虑我们现在将 $n$ 分为了 $m$ 和 $\frac{n}{m}$ , 直接递归两侧继续分解即可。 复杂度为什么对呢? 我们考虑分解的过程,每一次分解的复杂度是 $O(\sqrt p)$ , 最终分解为了 $n$ 的所有质因数,假设有 $k$ 个(由于这些质因子可以重复所以不是 $\omega(n)$ 个),设为 $p_1,p_2\dots p_k(p1\le p2\le\dots\le p_k)$,而分解的递归树是一棵二叉树,底层叶子是每个质因子,贡献复杂度的是每个非叶子节点,贡献为子树中最小的 $p$ 开根号,由于二叉树叶子数恒定,所以非叶节点共 $k-1$ 个,欲使最大化复杂度看最劣情况,即从小到大每个只贡献一次,所以复杂度是 $O(\sum\limits_{i=1}^{k-1}\sqrt p_i)$ 的,因为 $p_i\ge 2$ ,且有 $\prod\limits_{i=1}^{k} p_i=n$ , 由不等式知识知两项且相等时取到最大值,即 $O(\sqrt p_1)=O(n^{\frac{1}{4}})$ 。 求 $\gcd$ 的 $\log$ 被那个 127 次相乘判断一次的抵消了,即最后复杂度期望是 $O(n^{\frac{1}{4}}\frac{\log n}{127})$ 。 ```cpp #include <bits/stdc++.h> using namespace std; #define ll long long #define lll __int128 ll mod; inline ll add(ll x,ll y) { ll res=x+y; return res<mod?res:res-mod; } inline ll mul(ll x,ll y) { return (lll)x*y%mod; } inline ll qpow(ll x,ll y) { ll res=1; while(y) { if(y&1)res=mul(res,x); x=mul(x,x),y>>=1; } return res; } ll pri[]={2,3,5,7,11,13,17,19,23,29,31,37}; inline bool chk(ll x) { ll v=qpow(x,mod-1); if(v!=1)return 0; ll d=mod-1; while(!(d&1)) { d>>=1; v=qpow(x,d); if(v==mod-1)return 1; if(v!=1)return 0; } return 1; } inline bool MR(ll x) { mod=x; for(auto p:pri) { if(x==p)return 1; if(!chk(p))return 0; } return 1; } mt19937 rnd(time(0)); inline ll f(ll x,ll c) { return add(mul(x,x),c); } inline ll rho(ll n) { mod=n; ll x=rnd()%mod,c=rnd()%(mod-1)+1; ll p=1; for(ll i=2,j=2,d=x;;i++) { x=f(x,c),p=mul(p,abs(x-d)); if(i%127==0&&__gcd(p,n)!=1)return __gcd(p,n); if(i==j) { j<<=1,d=x; if(__gcd(p,n)!=1)return __gcd(p,n); } } } ll PR(ll n) { //cout<<n<<"\n"; if(MR(n))return n; ll p=n; while(p==n)p=rho(n); return max(PR(p),PR(n/p)); } inline void SOLVE() { ll n; cin>>n; ll ans=PR(n); if(ans==n)cout<<"Prime\n"; else cout<<ans<<"\n"; } signed main() { ios::sync_with_stdio(0); cin.tie(0); int t; cin>>t; while(t--)SOLVE(); return 0; } ```