Miller-Rabin 素性检验和 Pollard-Rho 算法
Tangninghaha · · 算法·理论
Miller-Rabin 素性检验
常规确定性素性检验的时间复杂度是
费马素性检验
由费马小定理,可以得到:若
该定理的逆否命题:若存在
借此启发我们随机找一些底数
然而遗憾的是,有一些合数能够通过某些费马素性验证。例如当
更为惊人的是,有一些满足对于所有
所以我们需要对费马素性检验进行改进。
二次探测定理
内容
如果
证明
由原式可以导出
由于
Miller-Rabin 素性测试
结合费马素性检验和二次探测定理可以得到最终的素性检验。
我们称
如果我们找到
一个偶数的素性测试是容易的。
对于一个奇数
对于一个随机的
时间复杂度分析
由于对
如果对乘法使用快速傅里叶变换算法进行优化,单次乘法操作复杂度降为
假设使用朴素算法进行
错误率分析
该算法的错误率取决于有多少个底数,使得一个合数被错误地判定为素数。
而这样的底数占总数的比率不超过
实现
这个程序实现了对 long long 范围内的整数进行 miller-rabin 测试,测试 TEST_TIME 中设定的次数次。
std::mt19937_64 Rad(std::chrono::steady_clock::now().time_since_epoch().count());
using ll = long long;
ll qpow(ll a, ll b, ll p) {
ll res = 1;
for (; b; b >>= 1, a = (__int128)a * a % p)
if (b & 1) res = (__int128)res * a % p;
return res;
}
ll rad(ll l, ll r) {
return std::uniform_int_distribution<ll>(l, r)(Rad);
}
const int TEST_TIME = 8;
bool miller_rabin(ll n) {
if (n < 3) return n == 2;
if (n == 3) return true; // 判掉一些边边角角的情况
ll u = n - 1, t = 0;
while (!(u & 1)) ++t, u >>= 1;
for (int i = 1; i <= TEST_TIME; ++i) {
ll a = rad(2, n - 2);
ll v = qpow(a, u, n);
if (v == 1) continue; // 如果 v 为 1,直接通过测试
int s;
for (s = 0; s < t; ++s) {
if (v == n - 1) break; // 在某一次 v 为 n - 1 时,以后 v 都是 1,通过测试
v = v * v % n;
}
if (s == t) return false; // 当 s == t 时,未通过费马素性检验
}
return true;
}
拓展-确定性判素
假设广义 Riemann 猜想成立,那么进行确定性判素只需要选择
但是即使广义 Riemann 猜想成立,这样判定时间复杂度变为
但是在算法竞赛中,通常我们只需要对 long long 范围内的素数进行判定。在一定范围内,我们可以选用若干个固定底数进行确定性判素。例如[^4]:
-
对于
2^{32} 范围内的判素,选取2,7,61 三个数为底。 -
对于
2^{64} 范围内的判素,选取2,325,9375,28178,450775,9780504,1795265022 七个数为底。 -
如果记不住上面七个数,可以使用
2,3,5,7,11,13,17,19,23,29,31,37 十二个数(即前 12 个素数)为底,可以适用于2^{78} 之内的判素。
Pollard-Rho 算法
分解质因数有
同样地,对于更大规模的数据,朴素算法显得力不从心了,我们引入 Pollard-Rho 算法,该算法可以在期望
生日悖论
假设每年都是 365 天,求一个房间中至少多少人,才能使其中两个人生日相同的概率达到
设房间里有
可以代入几个数字得到:
| 23 | 41 | 57 | 100 | |
|---|---|---|---|---|
| 50.73\% | 90.32\% | 99.01\% |
可以求得若有 23 人,即可使得两个人生日相同概率达到
这样的数学事实与我们的观念差异很大,所以称之为悖论。
这启示我们,在一个区间内随机选取一些数字,其中含有相同数字的概率很高。
更具体地,当在
如果在
最大公约数求约数
如果我们直接在
我们巧妙地利用最大公约数的性质:假设有一个数字
满足
然而直接找满足条件的
注意到模
然而如果我们真的随机
Floyd 判环
下面将使用多项式函数
设
x=x'p+r,y=y'p+r ,那么f(x)=(x'^{2}p^{2}+r^{2}+2x'pr+c)\bmod{n}=x'^{2}p^{2}+r^{2}+2x'pr+c-kn ,同理得到f(y)=y'^{2}p^{2}+r^{2}+2y'pr+c-kn 。
实际上任何多项式函数都满足这个性质。但是我们希望项数比较少以更快地得到结果。此外,经过本人测试,如果直接使用
可以感受一下,这个性质保证了若
将任意一个数代入数列的第一项,就可以生成一个随机数列。但是由于模
考虑两个在操场上跑圈的人,当跑得快的人追上跑得慢的人时,至少遍历了整个环一圈。
设
至于到底因为是环上任意两个位置都不能找到答案,还是因为我们使用的方法只检测了部分的位置,导致我们找不到答案,我就不知道了。
下面给出 模板题 的示例代码,由于该做法常数较大,无法通过。
#include <chrono>
#include <cstdio>
#include <random>
std::mt19937_64 Rad(std::chrono::steady_clock::now().time_since_epoch().count());
using ll = long long;
ll rad(ll l, ll r) {
return std::uniform_int_distribution<ll>(l, r)(Rad);
}
const int TEST_TIME = 8;
ll qpow(ll a, ll b, ll p) {
ll res = 1;
for (; b; b >>= 1, a = a * a % p)
if (b & 1) res = res * a % p;
return res;
}
bool miller_rabin(ll n) {
if (n < 3) return n == 2;
if (n == 3) return true;
ll u = n - 1, t = 0;
while (!(u & 1)) ++t, u >>= 1;
for (int i = 1; i <= TEST_TIME; ++i) {
ll a = rad(2, n - 2);
ll v = qpow(a, u, n);
if (v == 1) continue;
int s;
for (s = 0; s < t; ++s) {
if (v == n - 1) break;
v = v * v % n;
}
if (s == t) return false;
}
return true;
}
ll f(ll x, ll a, ll n) {
return ((__int128)x * x + x + a) % n;
}
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a;
}
ll pollard_rho(ll n) {
ll a = rad(1, n);
ll x1, x2;
x1 = x2 = rad(0, n - 1);
while (true) {
x1 = f(x1, a, n);
x2 = f(f(x2, a, n), a, n);
if (x1 == x2) {
x1 = x2 = rad(0, n - 1);
a = rad(1, n);
continue;
}
ll d = gcd(abs(x1 - x2), n);
if (d > 1 && d < n) return d;
}
}
ll ans;
void find(ll n) {
if (miller_rabin(n)) {
ans = std::max(ans, n);
return;
}
ll p = pollard_rho(n);
find(p);
find(n / p);
}
int main() {
int tt;
scanf("%d", &tt);
while (tt--) {
ll n;
scanf("%lld", &n);
if (miller_rabin(n)) {
puts("Prime");
continue;
}
ans = 0;
find(n);
printf("%lld\n", ans);
}
}
倍增优化
单次调用
所以我们可以累积一些乘积之后再求
接着使用倍增进行优化。枚举一个步数
下面给出模板的代码:
#include <chrono>
#include <cstdio>
#include <random>
std::mt19937_64 Rad(std::chrono::steady_clock::now().time_since_epoch().count());
using ll = long long;
ll rad(ll l, ll r) {
return std::uniform_int_distribution<ll>(l, r)(Rad);
}
const int TEST_TIME = 8;
ll qpow(ll a, ll b, ll p) {
ll res = 1;
for (; b; b >>= 1, a = (__int128)a * a % p)
if (b & 1) res = (__int128)res * a % p;
return res;
}
bool miller_rabin(ll n) {
if (n < 3) return n == 2;
if (n == 3) return true;
ll u = n - 1, t = 0;
while (!(u & 1)) ++t, u >>= 1;
for (int i = 1; i <= TEST_TIME; ++i) {
ll a = rad(2, n - 2);
ll v = qpow(a, u, n);
if (v == 1) continue;
int s;
for (s = 0; s < t; ++s) {
if (v == n - 1) break;
v = (__int128)v * v % n;
}
if (s == t) return false;
}
return true;
}
ll f(ll x, ll a, ll n) {
return ((__int128)x * x + x + a) % n;
}
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a;
}
ll pollard_rho(ll n) {
ll a = rad(1, n);
ll s = 0, t = 0, val = 1;
for (ll goal = 1; ; goal <<= 1, s = t, val = 1) {
for (ll step = 1; step <= goal; ++step) {
t = f(t, a, n);
val = (__int128)val * abs(t - s) % n;
if (!val) return n;
if (step % 128 == 0) {
ll d = gcd(val, n);
if (d > 1) return d;
}
}
ll d = gcd(val, n);
if (d > 1) return d;
}
}
ll ans;
void find(ll n) {
if (n == 1) return;
if (miller_rabin(n)) {
ans = std::max(ans, n);
return;
}
ll p = pollard_rho(n);
find(p);
find(n / p);
}
int main() {
int tt;
scanf("%d", &tt);
while (tt--) {
ll n;
scanf("%lld", &n);
if (miller_rabin(n)) {
puts("Prime");
continue;
}
ans = 1;
find(n);
printf("%lld\n", ans);
}
}
参考资料与链接
[^1]: https://oi-wiki.org/math/number-theory/pollard-rho/#%E7%94%9F%E6%97%A5%E6%82%96%E8%AE%BA 分解质因数 - OI wiki
[^2]: https://www.zhihu.com/question/308322307/answer/574767625 如何编程判断一个数是否是质数? - Wiener ES的回答 - 知乎
[^3]: https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Accuracy Miller–Rabin primality test - Wikipedia
[^4]: https://www.luogu.com.cn/article/jr27vtto 题解 P4718/论 Miller-Rabin 算法的确定性化 - 洛谷专栏
[^5]: https://en.wikipedia.org/wiki/Birthday_problem#Probability_of_a_shared_birthday_\(collision) Birthday problem - wikipedia