1. 程式人生 > >篩選法求素數演算法

篩選法求素數演算法

篩選法生成質數表(素數表)的基本思想如下:

假設有一個數組存放整數2 ~ N,如下所示:

首先將2的倍數篩去(實際操作時可以將陣列對應的值設定為0),得到:

然後將3的倍數篩去,得到:

再一次將5的倍數篩去,7的倍數篩去,11的倍數篩去...........直到陣列中所有的數都是質數。


#include<iostream>

using namespace std;
const int MAXV = 10000; //素數表範圍
bool flag[MAXV+1]; //標誌一個數是否為素數
int prime[MAXV+1]; //素數表,下標從0開始
int size; //素數個數
void genPrime(int max)
{
memset(flag, true, sizeof(flag));
for(int i = 2; i <= max / 2; i++)
{
if(flag[i])
{
for(int j = i << 1 ; j <= max; j += i)
{
flag[j] = false;
}
}
}
for(int i = 2 ; i <= max; i++)
{
if(flag[i])
{
prime[size++] = i;
}
}
}
int main()
{
genPrime(MAXV);
return 0;
}
關於素數的演算法是資訊學競賽和程式設計競賽中常考的數論知識,在這裡我跟大家講一下尋找一定範圍內素數的幾個演算法。看了以後相信
對大家一定有幫助。
    正如大家都知道的那樣,一個數 n 如果是合數,那麼它的所有的因子不超過sqrt(n)--n的開方,那麼我們可以用這個性質用最直觀的方法
來求出小於等於n的所有的素數。
    num = 0;
    for(i=2; i<=n; i++)
    {  for(j=2; j<=sqrt(i); j++)
         if( j%i==0 ) break;
       if( j>sqrt(i) ) prime[num++] = i;  //這個prime[]是int型,跟下面講的不同。
    }
    這就是最一般的求解n以內素數的演算法。複雜度是o(n*sqrt(n)),如果n很小的話,這種演算法(其實這是不是演算法我都懷疑,沒有水平。當然沒
接觸過程式競賽之前我也只會這一種求n以內素數的方法。-_-~)不會耗時很多.
    但是當n很大的時候,比如n=10000000時,n*sqrt(n)>30000000000,數量級相當大。在一般的機子它不是一秒鐘跑不出結果,它是好幾分鐘都跑不
出結果,這可不是我瞎掰的,想鍛鍊耐心的同學不妨試一試~。。。。
    在程式設計競賽中就必須要設計出一種更好的演算法要求能在幾秒鐘甚至一秒鐘之內找出n以內的所有素數。於是就有了素數篩法。
    (我表達得不清楚的話不要罵我,見到我的時候扁我一頓我不說一句話。。。)
    素數篩法是這樣的:
    1.開一個大的bool型陣列prime[],大小就是n+1就可以了.先把所有的下標為奇數的標為true,下標為偶數的標為false.
    2.然後:
      for( i=3; i<=sqrt(n); i+=2 )
      {   if(prime)
          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) 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))的演算法。
我水平有限啦,自己分析不來。最有說服力的就是自己上機試一試。下面給出這兩個演算法的程式:
//最普通的方法:
#include<stdio.h>
#include<math.h>#define N 10000001
int prime[N];
int main()
{
    int i, j, num = 0;
for(i=2; i<N; i++)
    {  for(j=2; j<=sqrt(i); j++)
         if( j%i==0 ) break;
       if( j>sqrt(i) ) prime[num++] = i;
    }
for(i=2; i<100; i++) //由於輸出將佔用太多io時間,所以只輸出2-100內的素數。可以把100改為N
    if( prime )printf("%d ",i);
   
return 0;
}
//用了篩法的方法:
#include<stdio.h>
#include<math.h>
#define N 10000001
bool prime[N];
int main()
{
   int i, j;
   for(i=2; i<N; i++)
  if(i%2) prime=false;
  else prime=true;
   for(i=3; i<=sqrt(N); i+=2)
   {   if(prime)
       for(j=i+i; j<N; j+=i)
            prime=false;
   }
   for(i=2; i<100; i++)//由於輸出將佔用太多io時間,所以只輸出2-100內的素數。可以把100改為N
    if( prime )
         printf("%d ",i);
   return 0;
}
裝了vc的同學上機跑一下這兩個程式試一試。這個差別,絕對是天上地下。前面那個程式絕對是n分鐘黑屏的說。
另外,對於這樣的篩法,還可以進一步優化,就是bool型數組裡面只存奇數不存偶數。如定義prime[N],則0表示
3,1表示5,2表示7,3表示9...。如果prime[0]為true,則表示3時素數。prime[3]為false意味著9是合數。
這樣的優化不是簡單的減少了一半的迴圈時間,比如按照原始的篩法,陣列的下標就對應數。則在計算30以內素
數的時候3個步驟加起來走了15個單位時間。但是用這樣的優化則是這樣:
則由於只存3 5 7 9 11 13 15 17 19 21 23 25 27 29,只需要14個單元
第 1 步 把14個單元賦為true (每個單元代表的數是2*i+3,如第0單元代表3,第1單元代表5...)
第 2 步開始:
     i=0;  由於prime[0]=true, 把 [3], [6], [9], [12]標為false.
     i=1;  由於prime[1]=true, 把 [6], [11]標為false
     i=2  2*i+3>sqrt(30)演算法結束。
這樣優化以後總共只走6個單位時間。
當n相當大以後這樣的優化效果就更加明顯,效率絕對不僅僅是翻倍。
出了這樣的優化以外,另外在每一次用當前已得出的素數篩選後面的數的時候可以一步跳到已經被判定不是素數的
數後面,這樣就減少了大量的重複計算。(比如我們看到的,i=0與i=1時都標了[6],這個就是重複的計算。)
我們可以發現一個規律,那就是3(即i=0)是從下標為[3]的開始篩的,5(即i=1)是從下標為[11]開始篩的(因為[6]
已經被3篩過了)。然後如果n很大的話,繼續篩。7(i=2)本來應該從下標為[9]開始篩,但是由於[9]被篩過了,而
[16]也已經被5(i=1)篩過了。於是7(i=2)從[23](就是2*23+3=49)開始篩。
於是外圍迴圈為i時,記憶體迴圈的篩法是從 i+(2*i+3)*(i+1)即i*(2*i+6)+3開始篩的。
這個優化也對演算法複雜度的降低起到了很大的作用。
相比於一般的篩法,加入這兩個優化後的篩法要高效很多。高興去的同學可以試著自己編寫程式看一看效率。我這裡
有程式,需要的可以向我要。不懂得也可以問我。
上面的素數篩法是所有程式設計競賽隊員都必須掌握的,而後面加了兩個優化的篩法是效率很高的演算法,是湖南大學
huicpc39同學設計的(可能是學來的,也可能是自創的。相當強悍)。在數量級更大的情況下就可以發現一般篩法和
優化後的篩法的明顯區別。
另外,臺灣的ACMTino同學也給我介紹了他的演算法:a是素數,則下一個起點是a*a,把後面的所有的a*a+2*i*a篩掉。
這上面的所有的素數篩選的演算法都可以再進一步化為二次篩選法,就是欲求n以內的素數,就先把sqrt(n)內的素數求
出來,用已經求得的素數來篩出後面的合數。
我把一般的篩選法的過程詳細的敘述了一遍,應該都懂了吧?後面的優化過程及不同的方法,能看懂最好。不是很難的。




相關知識:
最大公約數只有1和它本身的數叫做質數(素數)——這個應該知道吧?-_-b
    至今為止,沒有任何人發現素數的分佈規律,也沒有人能用一個公式計算出所有的素數。關於素數的很多的有趣的性質或者科學家的努力
我不在這裡多說,大家有興趣的話可以到百度或google搜一下。


1.高斯猜測,n以內的素數個數大約與n/ln(n)相當,或者說,當n很大時,兩者數量級相同。這就是著名的素數定理。  
2.十七世紀費馬猜測,2的2^n次方+1,n=0,1,2…時是素數,這樣的數叫費馬素數,可惜當n=5時,2^32+1就不是素數,
  至今也沒有找到第六個費馬素數。
3.18世紀發現的最大素數是2^31-1,19世紀發現的最大素數是2^127-1,20世紀末人類已知的最大素數是2^859433-1,用十進位制表示,這是一個258715位的數字。
4.孿生素數猜想:差為2的素數有無窮多對。目前知道的最大的孿生素數是1159142985×2^2304-1和1159142985×2^2304+1。
5. 歌德巴赫猜想:大於2的所有偶數均是兩個素數的和,大於5的所有奇數均是三個素數之和。其中第二個猜想是第一個的自然推論,因此歌德巴赫猜想又被稱為1+ 1問題。我國數學家陳景潤證明了1+2,即所有大於2的偶數都是一個素數和只有兩個素數因數的合數的和。國際上稱為陳氏定理。