2 条题解
-
1
有的时候,我们不想求前 个正素数,我们只想知道 是不是素数,此时可以使用素性检验.
素性检验分为两种:
- 确定性检验.
- 概率性检验.
后者通常比前者快很多,但是有很小的概率错误地将合数识别为素数(反之则不会),因此,通过概率素性测试的数字被称为可能素数,直到它们的素数可以被确定性地证明.
而通过测试但实际上是合数的数字则被称为伪素数,有许多特定类型的伪素数,最常见的是 Fermat 伪素数,它们是满足 Fermat 小定理的合数.
这篇笔记将重点叙述一种确定性检验:试除法和两种概率性检验:Fermat 素性测试和 Miller-Rabin 素性测试.
试除法
这种方法是枚举从小到大的每个小于 且不为 的数,并观察它是否能整除 :
int isPrime(int n) { if (n < 2) { return 0; } for (int i = 2; i < n; i ++) { if (n % i == 0) { return 0; } } return 1; }优化
这种做法十分稳妥,但是我们容易注意到如下事实:若 是 的约数,则 也是 的约数.
这个事实告诉我们,我们可以只考察 与 的其中一项. 方便起见,我们考察其中较小的一项,由此,我们只需要考察 中的整数即可,而 是任何数的约数,故不必考察 .
另一种较为朴素的优化思想是,我们只考虑对奇数进行判断,因为除了 以外的偶数一定有约数 . 此外, 的倍数也一定有约数 . 故综合以上叙述后,可考虑如下优化:
int isPrime(int n) { if (n < 3 || !(n % 2)) { return n == 2; } else if (!(n % 3)) { return n == 3; } for (int i = 2; (long long)i * i <= n; i ++) { if (n % i == 0) { return 0; } } return 1; }以上是关于试除法的叙述,在进行概率性检验的叙述前,需要先引入一条数论知识.
Fermat 小定理
该定理的内容如下: 设 是素数,对于任意整数 ,若有 ,则有
另一种等价叙述是: 设 是素数,对于任意整数 ,都有
以下是直接证明:
设 是素数且 ,首先证明:对于 ,余数 各不相同. 考虑反证法:如果存在 使得
$$ia\space\mathrm{mod}\space p=ja\space\mathrm{mod}\space p $$可得 $\left(i-j\right)a\equiv0\left(\mathrm{mod}\space p\right)$,而 与 显然都不整除 ,因此矛盾. 所以对于 ,余数 各不相同,也即 是 的一个排列,因此
$$\prod_{i=1}^{p-1}i=\prod_{i=1}^{p-1}ia\space\mathrm{mod}\space p\equiv\prod_{i=1}^{p-1}ia=a^{p-1}\prod_{i=1}^{p-1}i\left(\mathrm{mod}\space p\right) $$这说明
$$\left(a^{p-1}-1\right)\prod_{i=1}^{p-1}i\equiv0\left(\mathrm{mod}\space p\right) $$也就是说,$\displaystyle p\mid\left(a^{p-1}-1\right)\prod_{i=1}^{p-1}i$,而 都不是 的倍数,所以 ,即Fermat 小定理成立.
Fermat 小定理的逆命题并不成立,即使对于所有与 互素的 ,都有 ,那么 也未必是素数. 这也是 Fermat 素性测试中错误概率的来源.
Fermat 素性测试
Fermat 素性测试(Fermat primality test)是最简单的概率性素性检验.
我们可以根据 Fermat 小定理得出一种检验素数的思路,基本思想是不断地选取在 中的基底 ,并检验是否每次都有 :
int Fermat(int n) { if (n < 3 || !(n % 2)) { return n == 2; } else if (!(n % 3)) { return n == 3; } for (int i = 1; i <= testTime; i ++) { int a = rand() % (n - 2) + 2; if (quickPow(a, n - 1, n) != 1) { return 0; } } return 1; }上述代码中,参数
testTime是测试次数,一般建议设置为不小于 的整数,在保证效率的同时也保证正确率(不为 ).如果 但 不是素数,则称 为以 为底的 Fermat 伪素数.
在实践中我们观察到,如果 ,那么 通常是素数,但其实存在反例:对于 且 ,虽然有 ,但是 是合数.
事实上,对于任何固定的基底 ,这样的反例都有无穷多个.
既然对于单个基底,Fermat 素性测试无法保证正确性,一个自然的想法就是多检查几组基底. 但是,即使检查了所有可能的与 互素的基底 ,依然无法保证 是素数. 在上述条件下无法保证是素数的数称为 Carmichael 数,它也有无穷多个.
这迫使我们寻找更为严格的素性测试.
Miller-Rabin 素性测试
Miller-Rabin 素性测试(Miller-Rabin primality test)是一种更好的素数判定方法.
在之前我们已经提到过,要确保是素数,需要使用确定性算法,而确定性算法通常要慢得多. 然而,实际上没有已知的实际上是合数的数字通过了 Miller-Rabin 测试等高级概率性测试,因此我们可以放心使用.
在不考虑乘法的复杂度时,对数 进行 轮测试的时间复杂度是 .
Miller-Rabin 素性测试常用于对高精度数进行测试,此时时间复杂度是 ,利用 FFT 等技术可以优化到 .
为了解决 Carmichael 数带来的挑战,Miller-Rabin 素性测试进一步考虑了素数的如下性质:
二次探测定理:
若 是奇素数,则同余方程 的解只有 或者 .
容易验证, 为奇素数时, 与 都可以使上式成立. 由 Lagrange 定理可知,这就是该方程的所有解.
将 Fermat 小定理和二次探测定理结合起来使用,就能得到 Miller-Rabin 素性测试:
- 将 中的指数 分解为 .
- 在每轮测试中对随机出来的 先求出 ,之后对这个值执行最多 次平方操作.
- 在整个过程中,如果发现 的非平凡平方根(即除了 之外的其他根),就可以判断该数不是素数.
- 否则,再使用 Fermat 素性测试判断.
此处还有一些实现上的小细节:
- 对于一轮测试,如果某一时刻 $a^{u\times2^s}\equiv n-1\left(\mathrm{mod}\space n\right)$,则之后的平方操作全都会得到 ,则可以直接通过本轮测试.
- 如果找出了一个非平凡平方根 $a^{u\times2^s}\not\equiv n-1\left(\mathrm{mod}\space n\right)$,则之后的平方操作全都会得到 . 可以选择直接返回
false(0),也可以放到 次平方操作后再返回false(0).
由此一来,我们便得到了较正确的 Miller-Rabin 素性测试:
int millerRabin(int n) { if (n < 3 || !(n % 2)) { return n == 2; } else if (!(n % 3)) { return n == 3; } int u = n - 1, t = 0; while (u % 2 == 0) { t ++; u /= 2; } for (int i = 1; i <= testTime; i ++) { int a = rand() % (n - 3) + 2, v = quickPow(a, u, n) if (v == 1) { continue; } int s; for (s = 0; s < t; s ++) { if (v == n - 1) { break; } v = (long long)v * v % n; } if (s == t) { return 0; } } return 1; }以上代码中的参数
testTime的要求与 Fermat 素性测试中的相同.可以证明,奇合数 通过随机选取的一个基底 的 Miller-Rabin 素性测试的概率至多为四分之一. 因此,随机选取 个基底后,仍将合数误判为素数的概率不超过 .
在 OI 范围内,通常都是对 内的数作素性检验:
- 对于 内的数,选取 三个数作为基底进行 Miller-Rabin 素性检验就可以确定素性.
- 对于 内的数,选取 $\left\{2,325,9375,28178,450775,9780504,1795265022\right\}$ 七个数作为基底进行 Miller-Rabin 素性检验就可以确定素性.
注:如果要使用上面的数列中的数作为基底 判断 的素性:
- 所有的数都要取一遍.
- 需要把 换成 .
- 如果 或 ,则直接通过该轮测试.
以下是一个对于 内的数 正确的 Miller-Rabin 素性测试:
int millerRabin(int n) { if (n < 3 || !(n % 2)) { return n == 2; } else if (!(n % 3)) { return n == 3; } int u = n - 1, t = 0; while (u % 2 == 0) { t ++; u /= 2; } int tester[3] = {2, 7, 61}; for (int i = 0; i < 3; i ++) { int a = tester[i] % n, v = quickPow(a, u, n) if (a == 0 || a == n - 1 || a == 1) { continue; } if (v == 1) { continue; } int s; for (s = 0; s < t; s ++) { if (v == n - 1) { break; } v = (long long)v * v % n; } if (s == t) { return 0; } } return 1; }
信息
- ID
- 36
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 8
- 标签
- (无)
- 递交数
- 455
- 已通过
- 76
- 上传者