素数打表-筛法
1既不是素数也不是合数
打表:是一种典型的用空间换时间的做法,一般指将所有可能需要用到的结果事先计算出来,这样以后后面需要用到时就可以直接查表获得。在什么情况下我们需要打表?
(1)在程序中一次性计算出所有需要用到的结果,之后查询直接取这些结果。
举个例子,假如我们算Fibonacci数中的F(n)我们假如需要算很多次Q次 比如:(10^6),每次我们都是从头开始算的,对于Q次查询就会产生(nQ)的时间复杂度,但是如果我们提前把所有(足够大的n,大于题上给的范围)都事先算出来,并存在数组中,那么我们每次查询就只需要O(1)的时间复杂度,Q次查询就只需要O(Q+n)的时间复杂度,O(n)是预处理时间,
(2)在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。
这种用法一般是当程序的一部分过程消耗的时间过多(比如你的DFS(搜索),递归),或者是没有想好的算法,因此在另一个程序中使用暴力算法出结果,这样就能直接在源程序中使用这些结果(记忆化搜索,避免重复递归(搜索),你已经搜过一次用一个数保存在数组内,当下次再搜索到这里是直接调用这的值即可),比如对n皇后问题的方案数,如果饰演使用的算法不够好,就容易超时,而可以在本地程序计算出所有对n来说n皇后问题的方案数,然后把算出的结果保存在数组中,就可以根据题目输入的n来直接找出结果。
- 傻瓜式打表
每次判断这个数是否为素数,如果是就存在数组内,优点:太好想了,缺点:时间复杂度太高(n ^ 2)
1 #include<iostream> 2 #include<cmath> 3 using namespace std; 4 int main() { 5 int a[1005]; 6 int n; 7 bool flag; 8 int k = 0; 9 cin >> n; 10 for (int i = 2; i <= n; i++) { 11 flag = true; 12 for (int j = 2; j < i; j++) { 13 if (i % j == 0) { 14 flag = false; 15 break; 16 } 17 } 18 if (flag) { 19 a[k++] = i; 20 } 21 } 22 for (int i = 0; i < k; i++) { 23 cout << a[i] << " "; 24 } 25 return 0; 26 }
- 第二种进步版本
这个版本也很好想,我们算一个数是否是素数,是不是要判断它的因子,是否只是1和他本身,第一种傻瓜打表法我们从1 判断 到了n 本身,但是实际上好像不需要哦,我们只需要判断到 根号n就可以了。优点:稍微快了一点,缺点:时间复杂度还是(n^2)
1 #include<iostream> 2 #include<cmath.h> 3 using namespace std; 4 int main() { 5 int a[1005]; 6 int n; 7 bool flag; 8 int k = 0; 9 cin >> n; 10 for (int i = 2; i <= n; i++) { 11 flag = true; 12 for (int j = 2; j <= sqrt(i); j++) { 13 if (i % j == 0) { 14 flag = false; 15 break; 16 } 17 } 18 if (flag) { 19 a[k++] = i; 20 } 21 } 22 for (int i = 0; i < k; i++) { 23 cout << a[i] << " "; 24 } 25 return 0; 26 }
- 第三种进阶版本
我们想一想,我们上面的两种是从数的本身去判断是不是素数,那我们现在换一种方法。普通筛选法求素数-------埃拉托斯特尼筛法,出现一个数,则把以这个数为因子的数都标记为合数。
如2,所以4,6,8 10....都标记为合数
如3,所以9,12,15.....都标记为合数
如4,所以16,20,24...都标记为合数
即,若i是素数,则从 j=i*i 开始,把 j+i , j+2i , j+3i .....都标记为合数 (因为2*i , 3*i,4*i,....(i-1)*i 分别是2,3,4,...i-1的的倍数,已经在i之前标记过,所以从j=i*i开始标记)筛法的思想是去除要求范围内所有的合数,剩下的就是素数了,而任何合数都可以表示为素数的乘积,因此如果已知一个数为素数,则它的倍数都为合数。筛选法求素数,优点:O(nloglogn),缺点:因为6在i==2时就被标记了,而在i==3的时候又被标记了一次,所以还是有改进的空间。
1 #include<iostream> 2 #include<cmath> 3 using namespace std; 4 #define MAX_NUM 10000000 5 bool isPrime[MAX_NUM + 10]; 6 7 void prim() { 8 for (int i = 2; i <= MAX_NUM; i++) 9 isPrime[i] = 1; 10 for (int i = 2; i <= MAX_NUM; i++) { 11 if (isPrime[i]) 12 for (int j = 2 * i; j <= MAX_NUM; j += i) 13 isPrime[j] = 0; 14 } 15 for (int i = 2; i <= MAX_NUM; i++) { 16 if (isPrime[i]) 17 printf("%10d", i); 18 } 19 } 20 int main() { 21 prim(); 22 return 0; 23 }
这种方法是可以优化的,6是不是2 的倍数, 6也是3 的倍数,当你去除2的倍数的时候已经把6去掉了,然后又去去除3的倍数,又去了一次6,这是不是重复计算了。
- 线性筛法,即是筛选掉所有合数,留下质数
我们知道合数可以由一个质数数与另一个合数相乘得到
而同时假设合数a=质数b×质数c×一个数d
令e=c × d,假设b ≥ e,e为合数,令f=d × b
a=f × c ,其中c
即比一个合数数大的质数和该合数的乘积可用一个更大的合数和比其小的质数相乘得到(你从小的质数开始筛选,那么我们就可以找出)
这也是if(!( i % prime[j]))break;的含义,这也是线性筛法算质数表的关键所在
1 #include<iostream> 2 using namespace std; 3 #define MAXN 100000 4 #define MAXL 1000000 5 int prime[MAXN]; 6 bool check[MAXL]; 7 8 int main() { 9 int n, count; 10 while (cin >> n) { 11 memset(check, 0, sizeof(check)); 12 count = 0; 13 for (int i = 2; i <= n; i++) { 14 if (!check[i]) //如果是素数记录在素数表中 15 prime[count++] = i; //记录在数组中 16 for (int j = 0; j < count; j++) { 17 if (i * prime[j] > MAXL) { //判断是否越界 18 break; //过大时候跳出 19 } 20 check[i * prime[j]] = 1; //如果check[i*prime[j]]是已知素数的整数倍那一定是合数 21 if ((i % prime[j]) == 0) //如果i是一个合数,而且i % prime[j] == 0 22 break; 23 } 24 } 25 for (int i = 0; i < count; i++) 26 cout << prime[i] << " "; 27 } 28 return 0; 29 }