HackPluto's Blog

素数个数求解与素数的判定

字数统计: 3.3k阅读时长: 13 min
2019/05/22 Share

1.素数

质数(Prime number),又称素数,指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数(也可定义为只有1与该数本身两个正因数的数)。
对于写代码的人来说,素数比想像中的更重要,Google一下BigPrime或者big_prime你总会发现大堆大堆用到了素数常量的程序代码。平时没事时可以记一些素数下来以 备急用。我会选一些好记的素数,比如4567, 124567, 3214567, 23456789, 55566677, 1234567894987654321, 11111111111111111111111 (23个1)。
  素数有很多神奇的性质。我写4个在下面供大家欣赏。

1. 素数的个数无限多(不存在最大的素数)
  证明:反证法,假设存在最大的素数P,那么我们可以构造一个新的数2 3 5 7 … * P + 1(所有的素数乘起来加1)。显然这个数不能被任一素数整除(所有素数除它都余1),这说明我们找到了一个更大的素数。

2. 存在任意长的一段连续数,其中的所有数都是合数(相邻素数之间的间隔任意大)
  证明:当0<a<=n时,n!+a能被a整除。长度为n-1的数列n!+2, n!+3, n!+4, …, n!+n中,所有的数都是合数。这个结论对所有大于1的整数n都成立,而n可以取到任意大。

3. 所有大于2的素数都可以唯一地表示成两个平方数之差。
   证明:大于2的素数都是奇数。假设这个 数是2n+1。由于(n+1) ^ 2 = n ^ 2+2n+1,(n+1) ^ 2和n ^ 2就是我们要找的两个平方数。

4. 当n为大于2的整数时,2 ^ n+1和2^n-1两个数中,如果其中一个数是素数,那么另一个数一定是合数。
   证明:2 ^ n不能被3整除。如果它被3除余1,那么2 ^ n-1就能被3整除;如果被3除余2,那么2 ^ n+1就能被3整除。总之,2 ^ n+1和2^n-1中至少有一个是合数。

2.素数的基本定理

定理1:如果n不是素数,则n至少有一个( 1, sqrt(n) ]范围内的的因子

定理2:如果n不是素数,则n至少有一个(1, sqrt(n) ]范围内的素数因子

定理3:定义f(n)为不大于n的素数的个数,则 f(n) 近似等于 n/ln(n) (ln为自然对数)

2.1下面对定理三进行进一步的讲解:
素数的出现规律一直困惑着数学家。一个个地看,素数在正整数中的出现没有什么规律。可是总体地看,素数的个数竟然有规可循。对正实数x,定义π(x)为素数计数函数,亦即不大于x的素数个数。数学家找到了一些函数来估计π(x)的增长。以下是第一个这样的估计。

在这里插入图片描述
其中 ln x 为 x 的自然对数。上式的意思是当 x 趋近无限,π(x)与x/ln x的比值趋近 1。但这不表示它们的数值随着 x 增大而接近。注意到,上式并不是说指随着 x 趋近无限,π(x)和x/ln x的差趋近于 0。而是随着 x 趋近无限, π(x)和x/ln x的相对误差趋近于 0。
因此,素数定理也可以被想像成描述从正整数中抽到素数的概率:从不大于 n 的正整数中随机选出一个数,它是素数的概率大约是1/ln n。

下面是对π(x)更好的估计:

在这里插入图片描述,当x 趋近∞。其中 (对数积分),而关系式右边第二项是误差估计。

3.求不超过n的素数个数

3.1埃拉托斯特尼筛法
该算法的核心思想是:首先标记 2~ n 的数都为素数,然后遍历2~n的数组,如果它是素数,就把它的倍数标记为非素数(即把所有素数的倍数都标记为非素数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool num[10000];
int Num;
void Eratosthenes(int n)
{
for (int i=2; i<=n; i++) {
if (num[i]) {
printf("%d ",i);
Num++;
for (int j=i; j<=n; j+=i) {
num[j]=false;
}
}
}
printf("\n");
}
int main()
{
int n;
scanf("%d",&n);
for (int i=2; i<=n; i++) {
num[i]=true;
}
Eratosthenes(n);
printf("素数个数:%d\n",Num);

}

在这里插入图片描述
3.2快速线性筛法求素数
第二个方法是对于第一个的优化,在计算的过程中我们发现很多数被多次的剔除了比如,12在2的循环的时候被标记了一次false,在3的循环的时候又被标记了一次false,这显然是十分低效的。所以就有了第二个方法,快速线性筛法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int prime_num;
int prime[1000];
int num[10000];
void find_prime(int n)
{
for (int i=2; i<=n; i++) {
if (num[i]) {
prime[prime_num++]=i;
}
for (int j=0; j<prime_num&&i*prime[j]<=n; j++) {
num[i*prime[j]]=0;
if (i%prime[j]==0) {
break;
}
}
}
}
int main()
{
int n;
scanf("%d",&n);
for (int i=1; i<=n; i++) {
num[i]=1;
}
find_prime(n);
for (int i=0; i<prime_num; i++) {
printf("%d ",prime[i]);
}
printf("\n");
}

在这里插入图片描述
这个代码虽然从代码上看时间复杂度不是n,但是在实际的计算中确实是线性的在数据量大的时候比第一种方法快的多。这个代码的精髓就是

1
2
3
4
5
6
7
for (int j=0; j<prime_num&&i*prime[j]<=n; j++) {
num[i*prime[j]]=0;
if (i%prime[j]==0) {
break;
}
}
}

这段代码是不管这个数是不是素数都要执行的,因为如果i是素数,i和比i小的素数相乘就会得到新的数,如果i是合数那么i和比i的最小质因数相乘也会得到新的数。

3.3概率算法
本算法主要基于米勒-拉宾素性检验,米勒-拉宾素性检验是一种素数判定法则,利用随机化算法判断一个数是合数还是可能是素数。
两个基本定理
1.费尔马小定理:
如果p是一个素数,且0<a<p,则a^(p-1)%p=1,利用费尔马小定理,对于给定的整数n,可以设计素数判定算法,通过 计算d=a^(n-1)%n来判断n的素性,当d!=1时,n肯定不是素数,当d=1时,n很可能是素数.
2.二次探测定理:如果p是一个素数,且0<x<p,则方程x^2%p=1的解为:x=1或 x=p-1(易证)。

1819年有人发现了Fermat小定理逆命题的第一个反例:虽然2的340次方除以341余 1,但341=1131。后来,人们又发现了561, 645, 1105等数都表明a=2时Fermat小定理的逆命题不成立。虽然这样的数不多,但不能忽视它们的存在。于是,人们把所有能整除2^(n-1)-1的合 数n叫做伪素数(pseudoprime),意思就是告诉人们这个素数是假的。
不满足2 ^ (n-1) mod n = 1的n一定不是素数;如果满足的话则多半是素数。有 人自然会关心这样一个问题:伪素数的个数到底有多少?换句话说,如果我只计算2 ^ (n-1) mod n的值,事先不准备伪素数表,那么素性判断出错的概率有多少?研究这个问题是很有价值的,毕竟我们不可能背一个长度上千的常量数组带上考场。
统计表明,在前10亿个自然数中共有50847534个素数,而满足2^(n-1) mod n = 1的合数n有5597个。这样算下来,算法出错的可能性约为0.00011。这个概率太高了,如果想免去建立伪素数表的工作,我们需要改进素性判断的算 法。
最简单的想法就是,我们刚才只考虑了a=2的情况。对于式子a ^ (n-1) mod n,取不同的a可能导致不同的结果。一个合数可能在a=2时通过了测试,但a=3时的计算结果却排除了素数的可能。于是,人们扩展了伪素数的定义,称满足 a^(n-1) mod n = 1的合数n叫做以a为底的伪素数。前10亿个自然数中同时以2和3为底的伪素数只有1272个,这个数目不到刚才的1/4。这告诉我们如果同时验证a=2和a=3两种情况,算法出错 的概率降到了0.000025。容易想到,选择用来测试的a越多,算法越准确。通常我们的做法是,随机选择若干个小于待测数的正整数作为底数a进行若干次 测试,只要有一次没有通过测试就立即把这个数扔回合数的世界。
人们自然会想,如果考虑了所有小于n的底数a,出错的概率是否就可以降到0呢?没想 到的是,居然就有这样的合数,它可以通过所有a的测试(这个说法不准确,详见我在地核楼层的回复)。Carmichael第一个发现这样极端的伪素数,他 把它们称作Carmichael数。你一定会以为这样的数一定很大。错。第一个Carmichael数小得惊人,仅仅是一个三位数,561。前10亿个自 然数中Carmichael数也有600个之多。Carmichael数的存在说明,我们还需要继续加强素性判断的算法。
Miller和Rabin两个人的工作让素性测试迈出了革命性的一步,建立了传说中的Miller-Rabin素性测试算法。 新的测试基于下面的定理:如果p是素数,x是小于p的正整数,且x ^ 2 mod p = 1,那么要么x=1,要么x=p-1。这是显然的,因为x ^ 2 mod p = 1相当于p能整除x ^ 2-1,也即p能整除(x+1)(x-1)。由于p是素数,那么只可能是x-1能被p整除(此时x=1)或x+1能被p整除(此时 x=p-1)。我们下面来演示一下上面的定理如何应用在Fermat素性测试上。前面说过341可以通过以2为底的Fermat测试,因 为2 ^ 340 mod 341=1。如果341真是素数的话,那么2 ^ 170 mod 341只可能是1或340;当算得2 ^ 170 mod 341确实等于1时,我们可以继续查看2 ^ 85除以341的结果。我们发现,2^85 mod 341=32,这一结果说明了341是一个合数。
这就 是Miller-Rabin素性测试的方法。不断地提取指数n-1中的因子2,把n-1表示成d
2 ^ r(其中d是一个奇数)。那么我们需要计算的东西就 变成了a的d2 ^ r次方除以n的余数。于是,a ^ (d 2 ^ (r-1))要么等于1,要么等于n-1。如果a^(d 2 ^ (r-1))等于1,定理继续适用于a^(d 2 ^ (r-2)),这样不断开方开下去,直到对于某个i满足a^(d * 2 ^ i) mod n = n-1或者最后指数中的2用完了得到的a^d mod n=1或n-1。
Miller-Rabin 素性测试同样是不确定算法,我们把可以通过以a为底的Miller-Rabin测试的合数称作以a为底的强伪素数。第一个以2为底的强伪素数为2047。第一个以2和3为底的强伪素数则大到1 373 653。
Miller- Rabin算法的代码也非常简单:计算d和r的值(可以用位运算加速),然后二分计算a^d mod n的值,最后把它平方r次。程序的代码比想像中的更简单,我写一份放在下边。虽然我已经转C了,但我相信还有很多人看不懂C语言。我再写一次Pascal 吧。函数IsPrime返回对于特定的底数a,n是否是能通过测试。如果函数返回False,那说明n不是素数;如果函数返回True,那么n极有可能是 素数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std ;
#define rd(x) (rand()%(x))
typedef unsigned long long ll;

ll pow_mod(ll a,ll b,ll r)
{
ll ans=1,buff=a;
while(b)
{
if(b&1)
ans=(ans*buff)%r;
buff=(buff*buff)%r;
b>>=1;
}
return ans;
}

bool test(ll n,ll a,ll d)
{
if(n==2) return true;
if(n==a) return false;
if(!(n&1)) return false;
while(!(d&1)) d>>=1;
ll t = pow_mod(a,d,n);
while(d!=n-1&&t!=n-1&&t!=1){
t = t*t%n;//下面介绍防止溢出的办法,对应数据量为10^18次方;
d<<=1;
}
return t == n-1||(d&1)==1;//要么t能变成n-1,要么一开始t就等于1
}

bool isprime(ll n)
{
int a[] = {2,3,5,7};//或者自己生成[2,N-1]以内的随机数rand()%(n-2)+2
for(int i = 0; i <= 3; ++i){
if(n==a[i]) return true;
if(!test(n,a[i],n-1)) return false;
}
return true;
}
int main()
{
int t,ans=0;
ll N;
for(cin >> t;t;t--){
cin >> N;
cout << ((isprime(N))?"Yes":"No") <<endl;
}
}

CATALOG
  1. 1. 1.素数
  2. 2. 2.素数的基本定理
  3. 3. 3.求不超过n的素数个数