质数判定的常数优化
注意:下面可能有部分数学符号使用不规范,看懂就行。
如何迅速判断 \(n\) 是否为质数?
逊方法
枚举 \(i\) 满足 \(1 < i < n\),则 \(n\) 不是质数,当且仅当全部的 \(i \nmid n\)。
时间复杂度 \(O(n)\)。
bool isp(int n) //isp = is_prime
{
if (n <= 1) return false;
for (int i = 2; i < n; i++)
if (n % i == 0)
return false;
return true;
}
太慢了,考场绝对不要用。
大众化方法
若 \(i\mid n\),则 \(n \div i \mid n\)。
所以,\(i\) 实际只需枚举到 \(\sqrt{n}\)。
时间复杂度显然为 \(O(\sqrt{n})\)。
bool isp(int n) //isp = is_prime
{
if (n <= 1) return false;
//计算机处理乘法(或乘方)要比除法(或根式)要快。
//所以,i <= sqrt(n) 写成 i*i <= n 可以微小优化。
for (int i = 2; i*i <= n; i++)
if (n % i == 0)
return false;
return true;
}
效率还不错,大部分人都打这个。
*常数级优化大众化方法
不一定要了解。可以跳过。
方法二还不是最快的,主要原因是仍然有很多计算可以舍去。
例如说,如果 \(2 \nmid n\),显然 \(4 \nmid n\),这就是重复计算。
怎么避免重复计算呢?答案是尽可能地用质数检验。
我们考虑 \(\bmod 6\),对于 \(n \le 5\) 的情况可以直接特判掉。
看 \(n \ge 6\) 的情况。此时 \(n \bmod 6 = 0, 1, 2, 3, 4, 5\)。
容易想到,\(n \bmod 6 = 0, 2, 3, 4\) 必定不是质数,它们分别会被 \(6, 2, 3, 2\) 整除。
所以说,若 \(n\) 为质数,至少满足 \(n \bmod 6 = 1, 5\)。
所以,我们可以直接检验这些数,因为这样 \(i = \text{prime}\) 的概率会更大,枚举范围也减少了。
因此,我们可以直接枚举 \(i = 6c\) 满足 \(c \ge 1\),检验 \((i-1)\) 与 \((i+1)\) 即可。
注意,枚举时,方法二的技巧仍然要使用。
时间复杂度仍然是 \(O(\sqrt{n})\),所以其实快不到哪去。不过优化点常数对 Ynoi 还是很有帮助的!(虽然显然不会有这么简单的东西出现在 Ynoi 里
bool isp(int x)
{
if (x <= 1) return false;
if (x == 2 || x == 3) return true;
if (x % 6 != 1 && x % 6 != 5) return false;
for (int i = 6; i*i <= x; i += 6)
if (x % (i-1) == 0 || x % (i+1) == 0)
return false;
return true;
}
由于码量不会多多少,所以考试用这种优化方法是有好处的。
快飞了的超级方法
欢迎 Miller-Rabin 登场!!这个算法和上面几种方法的原理都不太相同!更加高级!
先给出模版:SP288 | HDU2138 | LOJ143
前置知识:
- 快速幂,\(O(\log y)\) 计算 \(x^y \bmod p\)。龟速乘(龟速乘看代码就能理解)。
- 一点点位运算知识。
费马小定理
大家都听说过,这里就不详述了,证明在这。
若 \(p\) 为质数,且 \(a\) 与 \(p\) 互质,则 \(a^ {p - 1} \equiv1 \pmod p\)。
但是不幸的是,逆命题不成立。也就是下面这个命题
如果存在一个 \(p\) 满足 \(a^ {p - 1} \equiv1 \pmod p\) ,则 \(p\) 一定是质数。
是错误的。存在一些合数作为反例。
尽管如此,Miller-Rabin 仍然是能依靠这个东西来完成质数的判断。
原因是,这些数并不是特别特别多。百度百科告诉你,
在 \(1\sim 10^8\) 范围内的整数中,只有 \(255\) 个反例。
虽然成功率不是 \(100\%\),但是只要选择的数恰当,就能让 long long
范围内的数全部判断正确。
选择的数是什么玩意?这个过一会再说,我们先来看第二个定理:二次探测定理。
二次探测定理
若 \(p\) 为质数且有 \(x^2 \equiv 1 \pmod p\),则 \(x = \pm 1\)。也就是 \(x = 1\) 或 \((p - 1)\)。
这个的证明非常简单,戳这里。
那么这个又有什么用呢?
结合思考
费马小定理中有一个 \((p - 1)\)。若 \(p\) 为(除了 \(2\) 外的)质数,
\((p - 1)\) 分解出 \(2\) ,\(p - 1 = 2^k \cdot n\),其中 \(k\) 必定大于等于 \(1\)。另外 \(n\) 是奇数,因为这个分解到底了。
于是,我们可以将费马小定理升级为:\(a^ {2^k \cdot n} \equiv1 \pmod p\)。其实显然和原本的一样
那么 \(a^ {2^k \cdot n}\) 可以写成 \((((a^n)^2)^2)^{2......}\),\(a^{2^{k+1} \cdot n}\) 就显然可以变成 \((a^ {2^k \cdot n})^2\)。
这玩意就长得很像二次探测定理。
为了让式子更加明显,我这里设 \(x = a^ {2^k \cdot n}\),则 \(x^2 = a^{2^{k+1} \cdot n}\)。
前面提到了,这东西是需要费马小定理逆命题来运算的。
具体证明过于复杂,我们可以草率地理解为:以这个逆命题来搞 Miller-Rabin。
我们设 \(x_2 = x^2 \bmod n\),即上面的 \(a^{2^{k+1} \cdot n}\)。若 \(x_2 = 1\) (费马小定理),倘若 \(p\) 是质数,\(x\) 应该是 \(1\) 或 \(n - 1\) (二次探测定理)。
综上,若 \(x_2 = 1\) 并且 \(x \ne 1, (n - 1)\),\(p\) 就一定不是质数。
最后,如果 \(x\) 不为 \(1\)(如果有过 \(1\),之后的 \(x\) 应该仍然是 \(1\)),根据费马小定理,这个数同样不是质数。
反之,尽管通过了测试,\(p\) 仍然有 \(\dfrac{1}{4}\) 的概率不是质数。这个 \(\dfrac{1}{4}\) 的来由,大家不要太执着,因为证明实在很困难。
于是,我们就可以写出 Miller-Rabin 的代码。(不是最终代码!!!!!)
//LL 是 long long
LL ksc(LL x, LL y, LL mod) //x * y
{
LL ans = 0;
while (y)
{
if (y & 1) ans = (ans + x) % mod;
x = (x + x) % mod;
y >>= 1;
}
return ans;
}
LL ksm(LL x, LL y, LL mod) //x^y
{
LL ans = 1;
while (y)
{
if (y & 1) ans = ksc(ans, x, mod);
x = ksc(x, x, mod);
y >>= 1;
}
return ans;
}
bool miller_rabin(LL n, int a) //费马小定理:n=质数,a^(n-1) mod n = 1
{
LL x = n - 1; int cnt = 0; //cnt 是 2 的幂的"个数"
while (x & 1 ^ 1) x >>= 1, cnt++; //x & 1 ^ 1 等价于 x % 2 == 0
x = ksm(a, x, n); //a^x,这里是二次利用 x
while (cnt--) //因为平方 cnt 次,才能变成 a^((2^x)*n)
{
LL x2 = ksc(x, x, n);
if (x2 == 1 && x != 1 && x != n - 1) return false; //如上分析
x = x2;
}
return x == 1; //如果 x != -1,return false
}
让 Miller-Rabin 正确率提高
前面说过了,只做一次测试,理论错误概率是 \(\dfrac{1}{4}\)。我们显然要多做几次测试。
但是,OI 界的判断质数,质数一定不会很大(基本就是 long long
范围)。
所以,我们可以巧妙地设定 \(a\),让准确率大大提高。
首先,设定 \(a\) 的值最好是质数。感性理解,就是做多次测试的 \(a\) 没有太多公共因数,每次筛掉的”伪质数“就会增多。
然后,在我们这个知识层面来说,就没有什么好方法了。
先人的结论是:用 \(\{2, 3, 5, 13, 17, 31, 37\}\) 这几个数来测试,long long
范围内全部正确。
那我们依次将 \(a\) 代入这些数即可,全部通过就理解为真正通过。另外,多设几个数测,也没啥问题,只是会变慢一点点罢了。
const int prime[7] = {2, 3, 5, 13, 17, 31, 37};
bool isprime(LL n)
{
if (n <= 1) return false; //特判
for (int i = 0; i < 7; i++)
{
if (n == prime[i]) return true; //必要的特判!很容易理解。
if (n % prime[i] == 0) return false; //不必要的特判,但可以在随机数据下大大提升效率。
if (!miller_rabin(n, prime[i])) return false;
}
return true;
}