质数判定的常数优化

注意:下面可能有部分数学符号使用不规范,看懂就行。

如何迅速判断 \(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;
}
posted @ 2022-08-26 01:58  liangbowen  阅读(172)  评论(0编辑  收藏  举报