初等数论学习笔记 II:分解质因数
CHANGE LOG
- 2022.7.13:重构文章,更新 PR 模板代码。
- 2023.1.23:对文章进行修补。
1. Miller-Rabin
Miller-Rabin 素性测试是一种具有随机性的素数判定方法。它有一定概率将合数判定为素数,但不会将素数判定为合数。
素数判定的基本思路为根据所有质数但很少合数具有的性质,检查被判定的数是否具有这些性质。若不具有,则该数是合数,否则该数大概率是质数。
1.1 费马素性检验
当
可惜命题并不成立。有极小概率使得
若
当面对形如
1.2 二次探测定理
根据二次剩余部分的知识,当
这被称为 二次探测定理。
1.3 算法介绍
结合费马素性检验与二次探测定理。
根据二次探测定理,当
一般地,若
这样检测的准确率很高。随机选择
Miller-Rabin 的效率与选取底数个数有关,我们希望减少底数并保证一定正确性。以下是常用底数,来自 wangrx 的博客。
- 对
以内的数判素,使用 三个底数。 - 对
以内的数判素,使用 七个底数。 - 使用前
个素数作为底数可对 以内的数判素。详见 A014233 - OEIS。 - 固定底数时需特判底数。若以
作为底数,则当 或 时直接通过检验,因为 无法通过以 为底的费马素性检验。
1.4 复杂度优化
Miller-Rabin 的复杂度和正确性足够优秀,但注意到整个过程中我们多次使用快速幂计算
考虑将整个过程反过来,即预先处理好
进一步地,先判掉
代码见 2.3 小节 P4718。
2. Pollard-Rho
分解质因数一般使用的试除法时间复杂度为
当
Pollard-Rho 为时间复杂度又开了一次平方,它可以在期望
2.1 生日悖论
从
公式含义为从
- 直观认知:当
增大时, 衰减很快,因为每次 以相乘的方式作用在 上。可以理解为指数衰减,但比指数衰减慢。如下图。
手玩函数图像后,我们发现使得
根据
因此,不严谨地,在
2.2 算法介绍
首先,我们必须有能力快速判断待分解的数是否为质数:Miller-Rabin。
Pollard-Rho 算法的精髓在于构造伪随机函数
因
根据生日悖论,模
因为整条路径类似希腊字母
若
同时,若模
记
注意,整个过程中我们 不知道
考虑从
,说明 即 的非平凡因子,直接返回。 ,说明进入 的循环节,大概率因为 的环长等于 的环长,也可能因为 的尾巴过长,使得第一次枚举到 环长的倍数使得 跳出 的尾巴时就枚举到了 的环长。无论如何,应结束本次失败的 Pollard-Rho,调整参数 重新分解。
上述算法称为基于 Floyd 判环的 Pollard-Rho 算法,期望时间复杂度
- 注意:笔者实现 Floyd 判环后,发现若令
则 无论 取何值均无法分解。需要特判 或从 开始计算 。 - 优化:
次数过多降低效率。朴素 Floyd 判环法无法通过模板题。考虑设置 样本累计上限 ,将 组 测试打包,将常数减小 倍。 在模板题数据下表现优秀。其正确性基于 。 - 优化:二分未知上界的数的最好方法是倍增。
较大时 太小了。考虑每检查一次就将 乘以 。同时为防止 过大无法及时检查,令 对 取较小值。一般取 。
如果读者担心
总之,
2.3 例题
P4718【模板】Pollard-Rho 算法
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
ll rd(ll l, ll r) {return rnd() % (r - l + 1) + l;}
ll ksm(ll a, ll b, ll p) {
ll s = 1;
while(b) {
if(b & 1) s = (__int128) s * a % p;
a = (__int128) a * a % p, b >>= 1;
}
return s;
}
bool Miller(ll n) {
if(n < 3 || n % 2 == 0) return n == 2;
ll r = n - 1, d = 0;
while(r & 1 ^ 1) r >>= 1, d++;
for(int _ = 0; _ < 10; _++) {
ll a = rd(2, n - 1), v = ksm(a, r, n);
if(v == 1) continue;
for(int i = 0; i <= d; i++) {
if(i == d) return 0;
if(v == n - 1) break;
v = (__int128) v * v % n;
}
}
return 1;
}
ll Pollard(ll n) {
ll c = rd(1, n - 1), s = c, t = 0;
auto f = [&](ll x) {return ((__int128) x * x + c) % n;};
ll acc = 0, prod = 1, d, limit = 1;
while(s != t) {
prod = (__int128) prod * abs(s - t) % n;
if(++acc == limit) {
if((d = __gcd(prod, n)) > 1) return d;
acc = 0, limit = min(127ll, limit << 1);
}
s = f(f(s)), t = f(t);
}
if((d = __gcd(prod, n)) > 1) return d;
return n;
}
ll mxp(ll n) {
if(Miller(n)) return n;
if(n == 1) return 1;
ll d = Pollard(n);
while(d == n) d = Pollard(n);
while(n % d == 0) n /= d;
return max(mxp(d), mxp(n));
}
int main() {
ios::sync_with_stdio(0);
ll T, n;
cin >> T;
while(T--) {
cin >> n;
if(Miller(n)) cout << "Prime\n";
else cout << mxp(n) << "\n";
}
return 0;
}
参考资料
第一章:
第二章:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!