ACM数论-素数
素数定义:
质数(prime number)又称素数,有无限个。质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数,这样的数称为质数。例 子:2、3、5、7、11、13、17、19。(那时候还有一种说法叫做“质数”,但是就语言上来说,我觉得“素数”这种叫法和“合数”比较搭配,类比于“化学元素”和“化合物”来看,叫“素数”非常贴切)
素数一些性质:
- 质数p的约数只有两个:1和p;
- 任一大于1的自然数,要么本身是质数,要么可以分解为几个质数之积,这种分解是唯一的;
- 一个偶数可以写成两个合数之和,其中每一个合数都最多只有9个质因数;
- 一个偶数必定可以写成一个质数加上一个合成数,其中合数的因子个数有上界;
素数应用:
- 数学上来看,质数有很多尚未证明的特性;应用上的话,公钥密码是一比较好的例子了。
- 素数对于数论就好像元素对于化学。(都摘自知乎)
判断素数:
1 //判断是否是一个素数 2 int IsPrime(int x) 3 { 4 if(x<=1)//0,1,负数都是非素数 5 return 0; 6 int bound=(int)sqrt(x)+1;
//计算枚举上界,为防止double值带来的精度损失,
所以采用根号值取整后再加1,即宁愿多枚举一个,也不愿少枚举一个数 7 for(int i=2; i<bound; i++) 8 { 9 if(x%i==0) 10 { 11 return 0; 12 } 13 } 14 return 1; 15 }
素数筛法是这样的:
1.开一个大的bool型数组prime[],大小就是n+1就可以了.先把所有的下标为奇数的标为true,下标为偶数的标为false.
2.然后:
for( i=3; i<=sqrt(n); i+=2 )
{ if(prime[i])
for( j=i+i; j<=n; j+=i ) prime[j]=false;
}
3.最后输出bool数组中的值为true的单元的下标,就是所求的n以内的素数了。
原理很简单,就是当i是质(素)数的时候,i的所有的倍数必然是合数。如果i已经被判断不是质数了,那么再找到i后面的质数来把这个质
数的倍数筛掉。
一个简单的筛素数的过程:n=30。
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
第 1 步过后2 4 ... 28 30这15个单元被标成false,其余为true。
第 2 步开始:
i=3; 由于prime[3]=true, 把prime[6], [9], [12], [15], [18], [21], [24], [27], [30]标为false.
i=4; 由于prime[4]=false,不在继续筛法步骤。
i=5; 由于prime[5]=true, 把prime[10],[15],[20],[25],[30]标为false.
i=6>sqrt(30)算法结束。
第 3 步把prime[]值为true的下标输出来:
for(i=2; i<=30; i++)
if(prime[i]) printf("%d ",i);
结果是 2 3 5 7 11 13 17 19 23 29
这就是最简单的素数筛选法,对于前面提到的10000000内的素数,用这个筛选法可以大大的降低时间复杂度。把一个只见黑屏的算法
优化到立竿见影,一下就得到结果。关于这个算法的时间复杂度,我不会描述,没看到过类似的记载。只知道算法书上如是说:前几年比
较好的算法的复杂度为o(n),空间复杂度为o(n^(1/2)/logn).另外还有时间复杂度为o(n/logn),但空间复杂度为O(n/(lognloglogn))的算法。
1 #define MAXSIZE 10001 2 3 int Mark[MAXSIZE]; 4 int prime[MAXSIZE]; 5 6 //判断是否是一个素数 Mark 标记数组 index 素数个数 7 int Prime(){ 8 int index = 0; 9 memset(Mark,0,sizeof(Mark)); 10 for(int i = 0;i < MAXSIZE;i++){ 11 //已被标记 12 if(Mark[i] == 1){ 13 continue; 14 } 15 else{ 16 //否则得到一个素数 17 prime[index++] = i; 18 //标记该素数的倍数为非素数 19 for(int j = i*i;j < MAXSIZE;j += i){ 20 Mark[j] = 1; 21 } 22 } 23 } 24 return index; 25 }
1 // 1:这是最原始的筛法,还有待优化 2 #define Max 1000000 3 bool prime[Max]; 4 void IsPrime(){ 5 prime[0]=prime[1]=0;prime[2]=1; 6 for(int i=3;i<max;i++) 7 prime[i]=i%2==0?0:1; 8 int t=(int)sqrt(Max*1.0); 9 for(int i=3;i<=t;i++) 10 if(prime[i]) 11 for(int j=i;j<Max;j+=i) 12 prime[j]=0; 13 } 14 //2:优化后的筛法,手动地模拟原始筛法就可以发现,某个数字可能被不止一次地删去 15 // 优化后的筛法就可以避免这种不必要的删去操作 16 #define Max 1000000 17 bool prime[Max]; 18 void IsPrime(){ 19 prime[0]=prime[1]=0;prime[2]=1; 20 for(int i=3;i<max;i++) 21 prime[i]=i%2==0?0:1; 22 int t=(int)sqrt(Max*1.0); 23 for(int i=3;i<=t;i++) 24 if(prime[i]) 25 for(int j=i*i;j<Max;j+=2*i)//优化 26 prime[j]=0; 27 }
1 //这就是素数的二次筛法,博士独创~~~~~ 2 //与前两种筛法不同,此种筛法中prime[i]=2*i+3(即:我们只存储奇数,偶数肯定不是素数的) 3 #define Max 1000000 4 bool prime[Max>>1]; 5 void IsPrime(){ 6 memset(prime,true,sizeof(prime)); 7 int n=Max>>1,m=(int)(sqrt(Max*1.0)/2.0); 8 for(int i=0;i<=m;i++) 9 if(prime[i]) 10 for(int j=2*i*i+6*i+3;j<=n;j+=2*i+3) 11 isprime[j]=false; 12 }
1 //函数模板 2 void make_prime() { 3 memset(prime, 1, sizeof(prime)); 4 prime[0]=false; 5 prime[1]=false; 6 int N=31700; 7 for (int i=2; i<N; i++) 8 if (prime[i]) { 9 primes[++cnt]=i; 10 for (int k=i*i; k<N; k+=i) 11 prime[k]=false; 12 } 13 return; 14 }
1 #include<stdio.h> 2 #include<math.h> 3 4 #define N 100000 5 6 int a[N]; 7 int s1[100000]; 8 9 int main() 10 { 11 int i,j,k,n; 12 13 for(i=0;i<=N;i++)//初始化表一 14 a[i]=1; 15 16 n=(int)sqrt(N);//注意n 17 for(i=2;i<=n;i++)//表一进行打表 18 { 19 for(j=i+i;j<=N;j+=i)//素数的倍数不是素数原理 20 a[j]=0; 21 } 22 23 k=1; 24 for(i=2;i<=N;i++)//将表一的素数存入表二,打表完成 25 if(a[i]) 26 { 27 s1[k]=i; 28 k++; 29 } 30 31 for(i=1;i<k;i++) 32 printf("%d\t",s1[i]); 33 34 return 0; 35 }
快速线性筛法
上面的方法比较好理解,初始时,假设全部都是素数,当找到一个素数时,显然这个素数乘上另外一个数之后都是合数
把这些合数都筛掉,即算法名字的由来。但仔细分析能发现,这种方法会造成重复筛除合数,影响效率。
比如10,在i=2的时候,k=2*15筛了一次;在i=5,k=5*6 的时候又筛了一次。所以,也就有了快速线性筛法。
利用了每个合数必有一个最小素因子。每个合数仅被它的最小素因子筛去正好一次。所以为线性时间。
函数模板如下:
1 void get_prime() 2 { 3 int cnt = 0; 4 for (int i = 2; i < N; i++) 5 { 6 if (!tag[i]) p[cnt++] = i; 7 for (int j = 0; j < cnt && p[j] * i < N; j++) 8 { 9 tag[i*p[j]] = 1; 10 if (i % p[j] == 0) 11 break; 12 } 13 } 14 }
1 int Mark[MAXSIZE]; 2 int prime[MAXSIZE]; 3 4 //判断是否是一个素数 Mark 标记数组 index 素数个数 5 int Prime(){ 6 int index = 0; 7 memset(Mark,0,sizeof(Mark)); 8 for(int i = 2; i < MAXSIZE; i++) 9 { 10 //如果未标记则得到一个素数 11 if(Mark[i] == 0){ 12 prime[index++] = i; 13 } 14 //标记目前得到的素数的i倍为非素数 15 for(int j = 0; j < index && prime[j] * i < MAXSIZE; j++) 16 { 17 Mark[i * prime[j]] = 1; 18 if(i % prime[j] == 0){ 19 break; 20 } 21 } 22 } 23 return index; 24 }
1 #include<iostream> 2 using namespace std; 3 const long N = 200000; 4 long prime[N] = {0},num_prime = 0; 5 int isNotPrime[N] = {1, 1}; 6 int main() 7 { 8 for(long i = 2 ; i < N ; i ++) 9 { 10 if(! isNotPrime[i]) 11 prime[num_prime ++]=i; 12 //关键处1 13 for(long j = 0 ; j < num_prime && i * prime[j] < N ; j ++) 14 { 15 isNotPrime[i * prime[j]] = 1; 16 if( !(i % prime[j] ) ) //关键处2 17 break; 18 } 19 } 20 return 0; 21 } 22 /*首先,先明确一个条件,任何合数都能表示成一系列素数的积。 23 不管 i 是否是素数,都会执行到“关键处1”, 24 ①如果 i 都是是素数的话,那简单,一个大的素数 i 乘以不大于 i 的素数,这样筛除的数跟之前的是不会重复的。筛出的数都是 N=p1*p2的形式, p1,p2之间不相等 25 26 ②如果 i 是合数,此时 i 可以表示成递增素数相乘 i=p1*p2*...*pn, pi都是素数(2<=i<=n), pi<=pj ( i<=j ) 27 p1是最小的系数。 28 根据“关键处2”的定义,当p1==prime[j] 的时候,筛除就终止了,也就是说,只能筛出不大于p1的质数*i。 29 我们可以直观地举个例子。i=2*3*5 30 此时能筛除 2*i ,不能筛除 3*i 31 如果能筛除3*i 的话,当 i' 等于 i'=3*3*5 时,筛除2*i' 就和前面重复了。 32 需要证明的东西: 33 一个数会不会被重复筛除。 34 合数肯定会被干掉。 35 根据上面红字的条件,现在分析一个数会不会被重复筛除。 36 设这个数为 x=p1*p2*...*pn, pi都是素数(1<=i<=n) , pi<=pj ( i<=j ) 37 当 i = 2 时,就是上面①的情况, 38 当 i >2 时, 就是上面②的情况, 对于 i ,第一个能满足筛除 x 的数 y 必然为 y=p2*p3...*pn(p2可以与p1相等或不等),而且满足条件的 y 有且只有一个。所以不会重复删除。 39 证明合数肯定会被干掉? 用归纳法吧。 40 41 类比一个模型,比如说我们要找出 n 中2个不同的数的所有组合 { i , j } ,1<=i<=n, 1<=j<=n, 42 我们会这么写 43 for (i=1; i<n; ++i ) 44 for (j=i+1; j<=n; ++j) 45 { 46 ///// 47 } 48 我们取 j=i+1 便能保证组合不会重复。快速筛法大概也是这个道理,不过这里比较难理解,没那么直观。 49 */
为什么这里要break?先可以得出一个结论,此时的prime[j]为(i*prime[j])的最小质因数。现设x=p1*a为合数,且p1为其最小的质因子,a为质数或合数,若为质数,则设a=1*p`(a`=1),否则设a=a`*p`,p`为质数(因为任意一个合数都可以表示成一个质数和另一个数的乘积)。则x=(p1*a`)*p`=p1*(a`*p`),且可以知道p1*a`<=p`*a`,即合数(p1*a`)小于合数(p`*a`),且p1<=p`,故得出结论,即比一个合数数大的质数和该合数的乘积可用一个更大的合数和比其小的质数相乘得到。
在上面实现代码中,i在1~maxp间循环,在i=p1*a时,若满足 !(i % prime[j])而不break的话,有可能在后面遇到一个prime[k],使得某个数x`=p1`*a`*prime[k],在之后i=(a`*prime[k])时,还会再和p1`相乘重新进行isNotPrime[i * prime[j]] = 1; 赋值,这样就造成了重复赋值,降低了效率,如果break了,则不会出现这样的情况。
考虑完上面的问题,还有个问题需要考虑,即省去的i*prime[k]在后来的过程中一定会再出现从而使得将isNotPrime[i * prime[k]]赋值为1么?答案是肯定的,因为上面提到了任意一个合数都可以表示成一个质数和另一个数的乘积,所以任意一个数总能表示成其最小质因子与另一个数相称的形式,因为我们枚举了所有可能的i,故“另一个数”的所有可能性我们都有考虑,所以,只要我们找到其最小质因子则就不会漏掉任意一个数。
//偶数显然不行,所以先去掉偶数。可以看作上面第一种的优化吧。
//不过这种方法不太直观,不太好理解。
1 我推荐这个算法! 易于理解。 只算奇数部分,时空效率都还不错! 2 half=SIZE/2; 3 int sn = (int) sqrt(SIZE); 4 for (i = 0; i < half; i++) 5 p[i] = true;// 初始化全部奇数为素数。p[0]对应3,即p[i]对应2*i+3 6 for (i = 0; i < sn; i++) { 7 if(p[i])//如果 i+i+3 是素数 8 { 9 for(k=i+i+3, j=k*i+k+i; j < half; j+=k) 10 // 筛法起点是 p[i]所对应素数的平方 k^2 11 // k^2在 p 中的位置是 k*i+k+i 12 // 下标 i k*i+k+i 13 //对应数值 k=i+i+3 k^2 14 p[j]=false; 15 } 16 } 17 //素数都存放在 p 数组中,p[i]=true代表 i+i+2 是素数。 18 //举例,3是素数,按3*3,3*5,3*7...的次序筛选,因为只保存奇数,所以不用删3*4,3*6....