素数判定方法

目录

  • 素数
    • 素数判断
      • 优化一
      • 优化二(强推)
    • 埃拉托斯特尼筛法
    • 欧拉筛法
    • Miller_Rabin 素性测试

素数

素数判断

素数是指在大于 1 1 1的自然数中,除了1和它本身之外不再有其他因数的自然数

判断一个数 n n n是否为素数自然可以利用这一点来,使用循环从2到n-1的位置,依次判断能否被这个数整除。

bool is_prime (int x) {
    if(x < 2) return false;
	for(int i = 2; i <= x - 1; ++i)
		if(x % i == 0)
			return false;
	return true;
}

优化一

实际上循环不需要到 n − 1 n-1 n1,到 n \sqrt{n} n 即可,因为对于整数 n n n的因子数 x x x,假设 n = x × y n = x \times y n=x×y,其中 x , y x,y xy这一对会被重复判断,例如 12 12 12的可以是 2 × 6 2 \times 6 2×6 , 3 × 4 3 \times 4 3×4 , 4 × 3 4 \times 3 4×3 , 6 × 2 6 \times 2 6×2,后面一半的计算都是多余的,如果 x x x表示左边的值, x x x最多判断到 n \sqrt{n} n 即可,超过这个值就是重复判断的。

bool is_prime (int x) {
	if(x < 2) return false;
	for(int i = 2; i * i <= x; ++i)
		if(x % i == 0)
			return false;
	return true;
}

优化二(强推)

大于等于 5 5 5的质数和 6 6 6的倍数相邻

可以证明:假设把所有大于等于 5 5 5的自然数表示成:

6 x − 1   、   6 x   、   6 x + 1   、   6 x + 2   、   6 x + 3   、   6 x + 4   、   6 x + 5   、   6 ( x + 1 )   、   6 ( x + 1 ) + 1 6x-1\,、\,6x\,、\,6x+1\,、\,6x+2\,、\,6x+3\,、\,6x+4\,、\,6x+5\,、\,6(x+1)\,、\,6(x+1)+1 6x16x6x+16x+26x+36x+46x+56(x+1)6(x+1)+1

可以看到除了 6 x − 1 6x-1 6x1 6 x + 1 6x+1 6x+1其他的数字

6 x + 2   、   6 x + 3   、   6 x + 4 6x+2\,、\,6x+3\,、\,6x+4 6x+26x+36x+4可以写成 2 ( 3 x + 1 )   、   3 ( 2 x + 1 )   、   2 ( 3 x + 2 ) 2(3x+1)\,、\,3(2x+1)\,、\,2(3x+2) 2(3x+1)3(2x+1)2(3x+2),因此一定不是素数( 6 x + 5 6x+5 6x+5看成 6 ( x + 1 ) − 1 6(x+1)-1 6(x+1)1,相当于 6 x − 1 6x -1 6x1)

即大于等于 5 5 5的质数一定是 6 x + 1 6x+1 6x+1或者 6 x − 1 6x-1 6x1的形式(和 6 6 6的倍数相邻),但是这些数不一定全都是素数,例如 35 = 5 × 7 = 6 ∗ 6 − 1 35 = 5 \times 7 = 6 * 6 - 1 35=5×7=661。所有 ( 6 x + 1 ) ∗ ( 6 x − 1 ) (6x+1)*(6x-1) (6x+1)(6x1)的数也可能满足在 6 6 6的两侧。因此需要判断

所以判断一个数 n n n是否是质数,小于 5 5 5的情况特判,同时考虑 n   %   6 n \, \% \,6 n%6的值一定是 1 1 1或者 5 5 5,否则肯定不是质数,到这里就保证了这个数一定在 6 6 6的倍数的两侧,接下来就只判断两侧的数是否能被 6 x + 1 6x+1 6x+1或者 6 x − 1 6x-1 6x1整除即可
因此循环从最小的 6 x − 1 6x-1 6x1开始,每次加 6 6 6,这样能枚举到所有 6 x − 1 6x-1 6x1的数,然后 6 x + 1 6x+1 6x+1 6 x − 1 + 2 6x-1+2 6x1+2来表示。

bool is_prime (int x) {
    if(x < 5) { //小于5的情况特判
    	if(x == 2 || x == 3) return true;
    	else return false;
    }
    if(x % 6 != 1 && x % 6 != 5) return false; //判断x是否在6的倍数的两侧
	for(int i = 5; i * i <= x; i += 6) //判断所有两侧的数中是否能被6i+1和6i-1整除
		if(x % i == 0 || x % (i+2) == 0) //i就是6k-1 i+2就是6k+1
			return false;
	return true;
}

埃拉托斯特尼筛法

如果 k k k是素数,那么 k k k的倍数一定不是素数

思想很简单,很好理解,筛法就是把不是素数的数筛掉,是素数的数保留下来。

因此要用一个 b o o l bool bool数组来记录这个数是否是素数,代码写起来很容易。

bool check[N] = {1, 1};  // 0, 1不是素数,素数标记为0,假设一开始除0,1都是素数
void Get_Prime (int n) {
	for(int i = 2; i * i <= n; i++)  //从2开始筛
		if(!check[i]) {
			for(int j = 2; j * i <= n; j++) //把所有素数i的倍数全部标记成1
				check[i*j] = 1;
		}
}

欧拉筛法

欧拉筛法才是真正意义上的线性筛法,对于前面的埃氏筛法,实际上对于一个数字,可能会被多次筛掉,例如 12 12 12,在找质数 2 2 2 3 3 3的时候都会被筛一次。效率很低,那么如果对于每一个数只筛一次呢?是不是才叫做真正的线性筛。

首先,任意一个合数都可以被分解成一个质数和一个数的积

假设,对于任意一个合数,我们用**“这个合数 = 最小质因数 × \times × 最大因数(非自己)”**,来表示

假设合数 A = X × Y A = X \times Y A=X×Y ( X X X表示最小质因数, Y Y Y表示最大因数(非自己))

可以让 Y Y Y 2 2 2开始循环,每次加 1 1 1,去乘所有得到质数。然后筛掉结果

但是, Y   %   X = = 0 Y \, \% \, X == 0 Y%X==0的时候,就要跳出循环,不能再筛了。

因为这个时候 Y Y Y X X X的倍数,如果把 X X X分解成 M × N M \times N M×N(其中 M M M表示最小质因数, N N N表示最大因数(非自己))

A = M × ( N × Y ) A = M \times (N \times Y) A=M×(N×Y),其中 M < X M < X M<X,很明显此时 A A A这个合数已经被 M M M更小的质因数筛掉了。 后面的都是重复计算了。

bool check[N] = {1, 1};  //0表示素数,1表示合数
int prime[M], cnt; //prime数组用来记录素数,cnt用来记录素数个数
void Get_Prime (int n) {
	for(int i = 2; i <= n; i++) {
		if(!check[i]) prime[++cnt] = i; //统计素数
		for(int j = 1; j <= cnt && j * i <= n; j++) { //取出所有的prime的素数来,筛掉合数
			check[i * prime[j]] = 1;
			if(i % prime[j] == 0) //说明已经被筛过了,直接跳出循环
				break;
		}
	}
}

Miller_Rabin 素性测试

前两种算法必须要从头开始筛选,不能离散的判断某一个数是否是素数。但是对于一个大数来讲,如果不能满足 O ( n ) O(\sqrt n) O(n )的算法,这时候可以进行Miller_Rabin素性测试,可大概率测出是否为素数

要使用这种算法,需要知道一个重要的定理:费马小定理

  • p p p为素数,对于任意整数 a a a( a a a不为 p p p的倍数,或者说 a a a p p p互质),都有 a p − 1 ≡ 1 ( m o d    p ) a^{p-1} \equiv 1(mod\,\,p) ap11(modp)

    这是一个充分条件,反过来不一定成立,即满足这个条件的 a p − 1 ≡ 1 ( m o d    p ) a^{p-1} \equiv 1 (mod \,\,p) ap11(modp),不一定能证明 p p p是一个素数,因此需要多次使用费马小定理来增强结果的可信度,但是还是有一类合数满足费马小定理的条件,例如 561 561 561,这类合数叫做 C a r m i c h a e l Carmichael Carmichael数。因此不能完全使用费马小定理来判断一个数是否是素数

  • 因此需要使用另外一个定理来避免将 C a r m i c h a e l Carmichael Carmichael数当成素数。二次探测定理

    如果 p p p是一个素数, 0 < x < p 0 < x < p 0<x<p,则方程 x 2 ≡ 1 ( m o d    p ) x^2 \equiv 1 (mod \,\,p) x21(modp)的解为 x = 1 x = 1 x=1 x = p − 1 x = p -1 x=p1

    可以简单证明: x 2 ≡ 1 ( m o d    p ) x^2 \equiv 1 (mod \,\,p) x21(modp)等价于 ( x − 1 ) ( x + 1 )   %   p = 0 (x-1)(x+1)\, \% \,p = 0 (x1)(x+1)%p=0,则要么 ( x + 1 )   %   p = 0 , x = p − 1 (x+1) \, \% \, p = 0,x = p-1 (x+1)%p=0x=p1,或者 ( x − 1 )   %   p = 0 , x = p + 1 (x-1) \, \% \,p = 0,x = p+1 (x1)%p=0x=p+1。因为 x < p x < p x<p。则不可能为 x = p + 1 x = p+ 1 x=p+1,此时 x x x可以取 1 1 1

    举例,对于一个满足费马小定理的合数 561 561 561,假设选择底数为 2 2 2,则 2 560   %   561 = 1 2^{560}\, \% \,561 =1 2560%561=1成立,这时候进行二次探测, x = 2 280 , 2 280   %   561 = 1 x = 2^{280},2^{280}\, \% \,561 = 1 x=22802280%561=1成立,继续探测 2 140   %   561 = 67 2^{140} \, \% \,561 = 67 2140%561=67,此时不等于 1 1 1或者 560 ( p − 1 ) 560(p-1) 560(p1),可以判断 561 561 561不是一个素数。那么对于 p p p我们可以将 p − 1 p-1 p1分解成 p − 1 = 2 t × r p-1 = 2^t \times r p1=2t×r,对于底数为 a a a的情况,首先算出 a r a^r ar,然后可以将 a r a^r ar不断平方,使用二次探测,变成 a 2 r 、 a 4 r . . . a^{2r}、a^{4r}... a2ra4r...,不断地模 p − 1 p-1 p1,观测当结果为 1 1 1的时候,此时 a r ∗ 2 t − i a^{r*2^{t-i}} ar2ti是否等于 p − 1 p-1 p1或者 1 1 1

因此对于一个数 n n n使用Miller_Rabin算法进行测试的时候

  • n n n的奇偶性,偶数直接舍去,以及小于等于 2 2 2的数特判
  • 选择一个质数 a a a作为底数,同时将 n − 1 n-1 n1分解成 2 t × r 2^t \times r 2t×r的形式
  • a t a^t at计算出来,然后进行 t t t次探测,不断乘下去
  • 最后得到 a n − 1   %   n a^{n-1}\, \%\,n an1%n的形式,根据费马小定理,判断结果是否为 1 1 1

总结

使用两个定理选择合适的底数,增加正确率

  • 定理一(费马小定理):若 p p p为素数,对于任意整数 a a a( a a a不为 p p p的倍数,或者说 a a a p p p互质),都有 a p − 1 ≡ 1 ( m o d    p ) a^{p-1} \equiv 1(mod\,\,p) ap11(modp)
  • 定理二(二次探测定理):如果 p p p是一个素数, 0 < x < p 0 < x < p 0<x<p,则方程 x 2 ≡ 1 ( m o d    p ) x^2 \equiv 1 (mod \,\,p) x21(modp)的解为 x = 1 x = 1 x=1 x = p − 1 x = p -1 x=p1
    通过选择不同的底数,不断使用二次探测定理,增加判断的正确率,而二次探测就是为了筛掉那些符合费马小定理的合数(伪素数),算法核心表述出来就是将要判断的数 n n n表示成 n − 1 = 2 t ∗ r n-1 = 2^t * r n1=2tr,这一步可以使用位运算加速,然后先计算出 b = p r i m e r   m o d   n b = prime^r\, mod\,n b=primermodn然后把 b b b平方 r r r次,进行判断,最后验证一下费马小定理即可

a a a可以选择多次,如果取遍 30 30 30以内的整数,int范围内的素数判断不会出错,同时在进行 a r × a r a^r \times a^r ar×ar时候。如果是long long 范围的,结果可能出错,需要使用快速积

模板题(Loj 143. 质数判定)

快速乘一定要用 O ( 1 ) O(1) O(1)的方法不然会超时

#include <cstdio>
#include <iostream>
#define ll long long
#define T 9
using namespace std;
ll prime[T] = {2, 3, 5, 7, 11, 13, 17, 19, 23};

ll Quick_Multiply (ll x, ll y, ll mod) {
	ll tmp = x * y - ((ll)((long double)x / mod * y + 0.5)) * mod;
    return tmp < 0 ? tmp + mod : tmp; 
}

ll Quick_power (ll a, ll b, ll c) {
	a %= c;
	ll res = 1;
	while (b) {
		if(b & 1) res = Quick_Multiply (res, a, c);
		b >>= 1;
		a = Quick_Multiply (a, a, c);
	}
	return res;
}

bool Miller_Rabin (ll p) {
	if (p == 2) return true;
	if (!(p & 1) || p < 2) return false;
	ll k, r = p - 1, t = 0, b;
	while (!(r & 1)) { // 计算p-1 = r * 2^t
		r >>= 1;
		t ++;
	}
	for (int i = 0; i < T; i++) {
		if(prime[i] == p) return true;
		k = Quick_power (prime[i], r, p); 
		b = k;
		for (int j = 0; j < t; j++) {
			b = Quick_Multiply (k, k, p); //二次探测
			if (b == 1 && k != 1 && k != p-1) return false;
			k = b;
		}
		if(b != 1) return false; //费马小定理判断
	}
	return true;
}

int main () {
	ll n;
	while (scanf("%lld", &n) == 1) {
		if (Miller_Rabin(n)) 
			printf("Y\n");
		else 
			printf("N\n");
	}
	return 0;
}

你可能感兴趣的:(数学,一些技巧)