素数筛法是数论的入门,当然也非常重要。所谓素数(也叫质数),就是因数只有1和它本身的数。
今天讲一下怎么筛素数。
第一种算法,就是最朴素最暴力的算法,是人的都会。就是对于每一个数n,从 i = 2开始,依次判断n能否被i整除。
1 bool judge(int x) 2 { 3 for(int i = 2; i * i <= x; ++i) //只用判断到sqrt(n)就行 4 if(x % i == 0) return false; 5 return true; 6 } 7 void pusushaifa(int n) 8 { 9 for(int i = 2; i <= n; ++i) 10 if(judge(i)) printf("%d ", i); 11 printf(" "); 12 }
好想的当然也就慢,这个算法复杂度是O(nlogn),当n为1e7 时就过不了了。
第二种算法,Eratosthenes 筛法,简称埃氏筛法。这个算法也很好理解:对于不超过 n 的非负整数 p,删除2p, 3p, 4p……,当处理完所有数据后,没被删除的就是素数。
用 vis[i] = 1 表示 i 被删除,代码就可以写出来了
1 const int maxn = 1e7 + 5; 2 int vis[maxn]; 3 void erato1(int n) 4 { 5 memset(vis, 0, sizeof(vis)); 6 for(int i = 2; i <= n; ++i) 7 for(int j = 2 * i; j <= n; j += i) vis[j] = 1; 8 for(int i = 2; i <= n; ++i) 9 if(!vis[i]) printf("%d ", i); 10 printf(" "); 11 }
我们分析一下复杂度:内层循环的次数是 (floor) n / i - 1 < n / i,所以内层循环的总次数一定小于 n / 2 + n / 3 + n / 4 +……+ n / n。因为 n / 2 + n / 3 < n,n / 4 + n / 5 + n / 6 + n / 7 < n,…… ,所以时间复杂度小于 O(nlogn)。
这个算法还可以优化,“对于不超过 n 的非负整数 p”,p可以限定为素数,而且内层循环必不从 i * 2 开始,因为他已在 i = 2 时就筛掉了。
1 const int maxn = 1e7 + 5; 2 int vis[maxn]; 3 void erato2(int n) 4 { 5 memset(vis, 0, sizeof(vis)); 6 for(int i = 2; i * i <= n; ++i) if(!vis[i]) //是素数 7 for(int j = i * i; j <= n; j += i) vis[j] = 1; 8 for(int i = 2; i <= n; ++i) 9 if(!vis[i]) printf("%d ", i); 10 printf(" "); 11 }
改进后的复杂度就变成了 O(nloglogn)。
还有第三种一个很牛的算法,线性筛法。我们先回顾一下埃氏,埃氏算法的一个弊端就是一个数 n 被它的所有素因子筛了一遍,重复筛导致浪费时间。
我们如何保证每一个数只被筛一遍呢?有一种想法,只要让每一个数只被他的最小素因子筛去就行。
写代码时,开一个数组标记,和上面的埃氏筛法的数组相同,再开一个数组记录素数,用来表示某一个数的最小素因子。代码如下
1 int notPrime[maxn], prime[maxn / 10]; 2 void xianxingshai(int n) 3 { 4 for(int i = 2; i <= n; ++i) 5 { 6 if(!notPrime[i]) {prime[++prime[0]] = i; printf("%d ", i);} //如果是素数,就存下来 7 for(int j = 1; j <= prime[0] && prime[j] * i <= n; ++j) //prime[j] * i就表示要筛的那个数 8 { 9 notPrime[prime[j] * i] = prime[j]; //标记 10 if(i % prime[j] == 0) break; 11 } 12 } 13 printf(" "); 14 }
第9行拿prime[j]标记,因为素数队列是严格递增的,因此notPrime[j] 表示的也是 j 的最小素因子。
特别强调的是第10行,若 i 能被prime[j] 整除,则 i = prime[j] * x,所以 i * prime[j + m] = prime[j] * x * prime[j + m],因此 i * prime[j + m] 一定被 prime[j] 筛去,就不必再重复筛了。
还有的就是,这个 i 一定是一个素数,因为如果是一个合数的话,那么他一定等于一个小于他的素数乘以另一个数,而这个素数一定在他之前就被遍历到了。
因为每一个数只被筛去一次,所以时间复杂度就是 O(n)。
特别鸣谢:http://www.cnblogs.com/suno/archive/2008/02/04/1064368.html,线性筛法这讲得很透。