素数是指在大于 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 n−1,到 n \sqrt{n} n即可,因为对于整数 n n n的因子数 x x x,假设 n = x × y n = x \times y n=x×y,其中 x , y x,y x,y这一对会被重复判断,例如 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 6x−1、6x、6x+1、6x+2、6x+3、6x+4、6x+5、6(x+1)、6(x+1)+1
可以看到除了 6 x − 1 6x-1 6x−1和 6 x + 1 6x+1 6x+1其他的数字
6 x + 2   、   6 x + 3   、   6 x + 4 6x+2\,、\,6x+3\,、\,6x+4 6x+2、6x+3、6x+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 6x−1)
即大于等于 5 5 5的质数一定是 6 x + 1 6x+1 6x+1或者 6 x − 1 6x-1 6x−1的形式(和 6 6 6的倍数相邻),但是这些数不一定全都是素数,例如 35 = 5 × 7 = 6 ∗ 6 − 1 35 = 5 \times 7 = 6 * 6 - 1 35=5×7=6∗6−1。所有 ( 6 x + 1 ) ∗ ( 6 x − 1 ) (6x+1)*(6x-1) (6x+1)∗(6x−1)的数也可能满足在 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 6x−1整除即可
因此循环从最小的 6 x − 1 6x-1 6x−1开始,每次加 6 6 6,这样能枚举到所有 6 x − 1 6x-1 6x−1的数,然后 6 x + 1 6x+1 6x+1用 6 x − 1 + 2 6x-1+2 6x−1+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;
}
}
}
前两种算法必须要从头开始筛选,不能离散的判断某一个数是否是素数。但是对于一个大数来讲,如果不能满足 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) ap−1≡1(modp)。
这是一个充分条件,反过来不一定成立,即满足这个条件的 a p − 1 ≡ 1 ( m o d    p ) a^{p-1} \equiv 1 (mod \,\,p) ap−1≡1(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) x2≡1(modp)的解为 x = 1 x = 1 x=1 或 x = p − 1 x = p -1 x=p−1
可以简单证明: x 2 ≡ 1 ( m o d    p ) x^2 \equiv 1 (mod \,\,p) x2≡1(modp)等价于 ( x − 1 ) ( x + 1 )   %   p = 0 (x-1)(x+1)\, \% \,p = 0 (x−1)(x+1)%p=0,则要么 ( x + 1 )   %   p = 0 , x = p − 1 (x+1) \, \% \, p = 0,x = p-1 (x+1)%p=0,x=p−1,或者 ( x − 1 )   %   p = 0 , x = p + 1 (x-1) \, \% \,p = 0,x = p+1 (x−1)%p=0,x=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=2280,2280%561=1成立,继续探测 2 140   %   561 = 67 2^{140} \, \% \,561 = 67 2140%561=67,此时不等于 1 1 1或者 560 ( p − 1 ) 560(p-1) 560(p−1),可以判断 561 561 561不是一个素数。那么对于 p p p我们可以将 p − 1 p-1 p−1分解成 p − 1 = 2 t × r p-1 = 2^t \times r p−1=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}... a2r、a4r...,不断地模 p − 1 p-1 p−1,观测当结果为 1 1 1的时候,此时 a r ∗ 2 t − i a^{r*2^{t-i}} ar∗2t−i是否等于 p − 1 p-1 p−1或者 1 1 1
因此对于一个数 n n n使用Miller_Rabin算法进行测试的时候
总结
使用两个定理选择合适的底数,增加正确率
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;
}