Pollard-Rho 学习笔记
距离省选只剩 2days 了,学学概念就差不多得了吧(
一言以蔽之,Pollard-Rho 是一种基于随机的、能够在期望 的时间内对一个数 分解质因数的算法。
那么 Pollard-Rho 究竟是如何运用随机的性质实现这一功能的呢?首先考虑一前置知识,如何使用随机化的知识判定一个数 是否是质数,也就是传说中的 Miller-Rabin 素性测试。
暴力随机 的因数显然是不可行的,毕竟随机到的概率太小了。考虑费马小定理:对于质数 ,有 ,其中 ,因此,我们如果找到了一个 ,满足 ,那么即可说明 不是质数。对于某些 ,运用此种方法确实能以较高的概率判定一个数是否是质数,因为对于固定的合数 ,如果至少存在一个 满足 ,那么满足 必定至少占了一半,考虑证明,显然 中的数与 意义下的乘法构成了一个群,而我们如果将 的 提取出来,那么它们与 意义下的乘法也构成了一个群,也就是说,满足 的 构成的群是 中的数的全体构成的群的子群,其大小为 的因数,又因为我们事先假定至少存在一个这样的 ,因此其大小至多占一半,因此对于这样的 ,其检验出来的概率是比较高的。
但是问题就来了,对于某些合数 ,不存在这样的 ,使得 ,这样一来,我们不论随机怎样的 ,都不可能把这样的 hack 掉,此时用上面的方法就不太行得通了。我们考虑另一个判定质数的准则,对于质数 ,有满足 的 只有 ,这条准则反过来说同样存在大量反例,即,存在不少合数,也有满足 的 只有 。但是如果我们把上面两条结合在一起,其判定质数的概率就增加了许多,即,我们考虑这样的过程:先特判 为偶数的情况,此时只有 是质数,其余都是合数。对于剩余的情况那么我们假设 。我们随机取 中某个数 并计算 ,如果是 则 hack 失败,否则我们不断平方直到平方到 为止,如果某一步得到了 则 hack 失败,否则则说明 是合数。可以证明该做法单次判定质数的概率高达 ,因此你随机取 个这样的 出错的概率就非常小了。这就是 Miller-Rabin 素性测试。
ll _rand() {return rand() & 32767;} ll Rand() {return (_rand() << 45) | (_rand() << 30) | (_rand() << 15) | _rand();} typedef __int128_t i128; const int LIM = 128; const int PTEST[10] = {0, 2, 3, 5, 7, 13, 19, 61, 233}; inline ll mul(ll a, ll b, ll mod) { ll r = ((long double) a / mod * b + 0.5); r = a * b - r * mod; return r < 0 ? (r + mod) : r; } ll qpow(ll x, ll e, ll mod) { ll ret = 1; for (; e; e >>= 1, x = mul(x, x, mod)) if (e & 1) ret = mul(ret, x, mod); return ret; } bool ptest(ll n) { if (n <= 500) { for (int i = 2; i * i <= n; i++) if (n % i == 0) return 0; return 1; } else { if (n % 2 == 0) return 0; ll tmp = n - 1; while (tmp % 2 == 0) tmp >>= 1; for (int i = 1; i <= 8; i++) { ll pw = qpow(PTEST[i], tmp, n); bool flg = 1; if (pw == 1 || pw == n - 1) flg = 0; while (tmp * 2 < n - 1) { tmp <<= 1; pw = mul(pw, pw, n); if (pw == n - 1) {flg = 0; break;} } if (flg) return 0; } return 1; } }
现在我们已经知道了如何判定一个大数是否是质数,考虑如何对一个大数分解质因数。考虑递归,如果 是质数则直接返回,这个可以一遍 MR 带走。否则我们考虑找到任意一个 的真因数(即, 且 的因数),然后递归分解 和 ,那么怎么找这个 呢?这就要用到一个非常著名的定理,生日悖论了。根据生日悖论,期望 人中能找到两个生日在同一天的人,更具体地,如果我们生成 个 中的数,那么期望有两个数相同。那么我们不妨假设 存在某个因子 ,那么在期望条件下,如果我们生成 个 中的数,会存在两个数模 同余,也就是说我们如果生成 个数,那么我们期望就能够找到 这个因子,而 的最小质因子 ,因此我们期望使用 的时间就能找到 的因子。
大体思想就是这么多,接下来就是具体实现的问题了。PR 找 的某个真因子大致有两种实现方式:追及法和倍增法。由于 rand()
开销较大,我们不妨考虑一个随机映射 ,其中 为在 PR 一开始确定的随机数,然后强制要求下一个生成的数为 ,其中 为上一个生成的随机数。下面将较为详细地讲解两种实现方法:
- 追及法:任取两个数 ,然后每次令 ,,如果某一步发现 就返回,根据上文,如果我们想找到因子 ,那么我们期望需要 步。
- 倍增法:考虑动态维护一个变量 ,然后进行 步,每次令 , 结束后令 ,同理,如果某一时刻 。
最后还有一点,如果直接按照上文所述实现,复杂度将会多一个 ,这将导致模板题无法通过,这里有一个非常使用的 trick:设一个阈值 (一般取 ),然后每 步一个单位,每次将 步得到的 乘起来, 结束后再与 取 ,如果发现与 的 大于 再回去找具体哪一步与 不互质。我的实现使用的是后者(也就是倍增法),即每 步取一步 , 步结束再取一个 ,实测跑得飞快。 次 级别的 Pollard-Rho 大约只用了 。
下面给出模板题代码:
ll _rand() {return rand() & 32767;} ll Rand() {return (_rand() << 45) | (_rand() << 30) | (_rand() << 15) | _rand();} typedef __int128_t i128; const int LIM = 128; const int PTEST[10] = {0, 2, 3, 5, 7, 13, 19, 61, 233}; inline ll mul(ll a, ll b, ll mod) { ll r = ((long double) a / mod * b + 0.5); r = a * b - r * mod; return r < 0 ? (r + mod) : r; } ll qpow(ll x, ll e, ll mod) { ll ret = 1; for (; e; e >>= 1, x = mul(x, x, mod)) if (e & 1) ret = mul(ret, x, mod); return ret; } bool ptest(ll n) { if (n <= 500) { for (int i = 2; i * i <= n; i++) if (n % i == 0) return 0; return 1; } else { if (n % 2 == 0) return 0; ll tmp = n - 1; while (tmp % 2 == 0) tmp >>= 1; for (int i = 1; i <= 8; i++) { ll pw = qpow(PTEST[i], tmp, n); bool flg = 1; if (pw == 1 || pw == n - 1) flg = 0; while (tmp * 2 < n - 1) { tmp <<= 1; pw = mul(pw, pw, n); if (pw == n - 1) {flg = 0; break;} } if (flg) return 0; } return 1; } } ll mx = 0; ll _gcd(ll x, ll y) {return (!y) ? x : _gcd(y, x % y);} ll finddiv(ll n, ll C) { ll c1 = Rand() % n + 1, c2 = c1; for (int st = 1; ; st <<= 1) { c2 = c1; ll prd = 1; for (int j = 1; j <= st; j++) { c1 = (mul(c1, c1, n) + C) % n; prd = mul(prd, abs(c1 - c2), n); if ((((j & 127) == 0) || j == st ) &&_gcd(prd, n) > 1) return _gcd(prd, n); } } return n; } void pollard_rho(ll n) { if (n <= mx) return; if (ptest(n)) {chkmax(mx, n); return;} ll v = finddiv(n, Rand() % (n - 1) + 1); pollard_rho(v); pollard_rho(n / v); } void solve() { ll n; scanf("%lld", &n); if (ptest(n)) {puts("Prime"); return;} mx = 0; pollard_rho(n); printf("%lld\n", mx); } int main() { int qu; scanf("%d", &qu); while (qu--) solve(); return 0; }
例题:
1. P5605 小A与两位神仙
首先考虑对 分解质因数,需要 Pollard-Rho,设 。
由于 ,因此我们不妨求出 在模 意义下的阶,不妨设之为 ,那么有结论:存在正整数 使得 当且仅当 。考虑证明:取 的原根 ,设 ,那么 ,那么 当且仅当 ,根据斐蜀定理,上式有解当且仅当 ,即 ,显然,这也等价于 。
于是这题思路就非常 ez 了,直接对 先分解一通质因数,然后 polylog 求阶后判一下整除性即可。
2. P6730 [WC2020] 猜数游戏
考虑转化题意:对于一组 ,如果存在正整数 使得 ,则连一条 的边,那么对于一组 ,其答案就是最小的集合 使得 ,且任意 中的元素要么属于集合 ,要么可以从集合 中某个元素一步到达。
由于我们连出来的这张图是一个传递闭包关系,即如果存在边 ,必然存在边 ,因此,对于最优集合 ,必然不存在 ,使得 ,因此我们考虑在每个点处计算贡献,对于一个点 ,其会对 集合对应的 的大小产生 的贡献,当且仅当其属于 ,但是能够到达 的点都不属于 。记能够到达 的点数为 ,那么 对答案的贡献为 。但是这样计算实际上是错误的,因为对于同一强连通分量中的点,我们有可能需要选择其中某个点加入集合 ,但由于其在同一强连通分量中,所有点都是可达的,我们并不会将其计入贡献,这其实好办,我们只用为该强连通分量中的点钦定一个顺序即可。
最后考虑如何判定是否存在正整数 使得 ,乍一看和上一题一样,但再仔细以看发现此题并没有保证 ,因此这题还要多一些特判——记 ,设 表示 质因数分解中 的次数,那么如果 则和上一题的情形一样,如果 中恰有一者非零则显然不存在这样的 ,否则必然有 ,ksm check 一下即可。
时间复杂度 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
2021-04-13 Atcoder Grand Contest 038 E - Gachapon(Min-Max 容斥+背包)
2021-04-13 常用函数泰勒展开式
2021-04-13 洛谷 P5643 - [PKUWC2018]随机游走(Min-Max 容斥+FWT+树上高斯消元,hot tea)