1. 程式人生 > >篩選法求素數(三種)

篩選法求素數(三種)

第一種:剔除2 3 4 5 6 ... ... 的倍數

在i從2開始的增一變化過程中,剔除i的倍數即j*i(j是大於等於2的自然數,j的上限是問題規模M)
為了減少重複步驟,可以每當i遞增到等於第一個沒有被剔除的(素)數時再剔除該數的倍數,
重複上述過程至i到達問題規模m的平方根+1

需要說明的三個問題:
假設迴圈到第n個數,如果該數沒有被剔除,那麼該數不能是前邊所有數的倍數,該數更不可能是後邊數的倍數,該

數就是素數。
如果該數是合數卻沒被剔除,那麼該數能分解為兩個小於該數的數的積的形式,而前邊剔除的數包含了所有小於該

數的數之間的積,這是矛盾的。

為什麼篩選迴圈的第一層只迴圈至問題規模m的平方根+1
因為,對於一個數m,所有大於該數平方根的數的積已經大於該數了,再剔除下去只是多餘。

為什麼篩選迴圈的第二層只迴圈至MAX/i?
因為此時j*MAX/i就等於MAX,此時需要標記為錯誤的數已經到了問題的規模即MAX,沒有必要在標記比MAX大的值不

是素數,此外用來標記i*j不是素數的陣列只有MAX+1的容量,這樣做是向不是自己申請的記憶體空間裡寫資料,是危

險的。

  1. <span style="font-size:13px;">#include <iostream>  
  2. #include <cstdlib>
  3. #include <cmath>
  4. usingnamespace std;  
  5. constint MAX=1000;  
  6. int main()  
  7. {  
  8.      int i=0,j=0,n=sqrt(MAX)+1;  
  9.      int a[MAX+1]={0};  
  10.      for(i=2;i<=n;i++)  //篩選迴圈
  11.        for(j=2;j<=MAX/i;j++)  
  12.            a[j*i]=1;  
  13.      for(i=2;i<=MAX;i++)  
  14.         if(a[i]==0)  
  15.         {  
  16.           cout.width(7);  
  17.           cout<<i<<" ";  
  18.         }  
  19.      system("pause");  
  20.      return
     0;  
  21. }  
  22. </span>  

用a[i*j]來標記i*j不是素數,這一個相對來說比較容易想到

再來看看下一個(第二種,稍作了優化)

  1. <span style="font-size:13px;">#include<stdio.h>  
  2. #include<math.h>
  3. #define MAX_P  500
  4. int nList[MAX_P] = {0};  
  5. void Calc()   
  6. {  
  7.     int n,p,t,sq=(int)sqrt(MAX_P*2+1);  
  8.     for (n=3;n<=sq;n+=2)  
  9.     {  
  10.         if (nList[n>>1]) continue;  
  11.         for (t=n*n;t<=MAX_P<<1;t+=n<<1) //篩選迴圈
  12.             nList[t>>1] = 1;  
  13.     }  
  14.     printf("%5d", 2);  
  15.     for (n=t=1;t<MAX_P;++t)  
  16.     {  
  17.         if (nList[t]) continue;  
  18.         printf("%5d", (t<<1)+1);  
  19.         if (++n==10)  
  20.         {  
  21.             printf("\n");  
  22.             n=0;  
  23.         }  
  24.     }  
  25. }  
  26. int main(void)  
  27. {  
  28.     Calc();  
  29.     getchar();  
  30.     return  0;  
  31. }</span>  

這個程式的方法是通過排除3 5 7 9 11 ... ...中的素數n的奇數倍來排除素數的
用陣列nList中的第[t/2]個元素的值(取1)來標記t不是素數。


1、為什麼是奇數的倍數?
首先我們假設這個演算法是正確的。由於素數除了2都是奇數,那麼每次送入篩選迴圈的n值都是奇數,排除t時t的遞

增表示式可寫為
for(int i=0;i<MAX_P/2;i++)
    t=n*(n+2i)
n是奇數n+2i也是奇數。這與演算法的思想是一致的。


2、為什麼3 5 7 9 11 ... ...中的素數n的倍數(奇數)要從n開始?

就是說從1開始和從n開始的效果是一樣,或者說n以前的奇數乘n都可以等於n以前(比n小的)素數的更大(比n大的

)奇數倍數。由於n是奇數,可以設n以前(比n小的)的奇數為n-2i,比n大的奇數為n+2j;那麼我們的任務就是,

證明n*(n-2i)可以等於m*(n+2j),其中m為小於n的素數,i、j都是正整數。即n(n-m)=2mj+2ni。這有變成證

明n-m是偶數,這是顯然的,兩個奇數之差必然是偶數。


3、為什麼篩選迴圈剔除了所有的非素數?

不管該演算法正確與否,3 5 7 9 11 ... ...中的任意n的奇數倍都排除了某些合數。
首先我們假設n迴圈至某個n時,為合數卻沒有剔除,那麼n可以分解因數為多個素數且是奇數(由於n是從3開始的奇

數)的積,當然也可表示為一個素數a與一個奇數b的積的形式,那麼這個a必然是小於n的素數。這個素數的奇數倍

就必然在前次迴圈中排除了。這與沒有被剔除矛盾。所以該演算法正確


最後我們來看看,第三種

  1. <span style="font-size:13px;">#define MAX_N 1000  
  2. int a[MAX_N+1];  
  3. int p[MAX_N+1];  
  4. int nCount=0;  
  5. void Init(int n) //線性篩法,不過在小範圍上(約n<1e7)不比上一個方法快
  6. {  
  7.     for (int i=2;i<=n;i++)  
  8.     {  
  9.         if (a[i]==0)  
  10.         {  
  11.             p[++nCount]=i;  
  12.         }  
  13.         for (int j=1,k; (j<=nCount) && (k=i*p[j])<=n; j++) //篩選迴圈
  14.         {  
  15.             a[k] = 1 ;  
  16.             if (i%p[j] == 0) break;  
  17.         }  
  18.     }  
  19. }  
  20. #include <iostream>
  21. int main(void)  
  22. {  
  23.     Init(MAX_N);  
  24.     for(int n=1; p[n]>1; ++n)  
  25.     {  
  26.         printf("%5d", p[n]);  
  27.     }  
  28.     return 0;  
  29. }</span>  

這一種可以說是對前種演算法的直接變形
用a[k]=1來標記k不是素數
第一種是用篩選出來的正確的數(即素數)的倍數剔除合數

第二種是用2到n乘篩選出正確的數,即素數

如果你不以為然,我可以把for (n=3;n<=sq;n+=2)該為(n=3;n<sq;n++)
著就和第一種演算法接近了一些,既然第一中和第二中被證明是正確的,那麼這一種就也是正確的。
如果能夠證明if (i%p[j] == 0) break;這一句新增進去是合理的

這句可解釋為當i第一次成為所挑選出來的正確的素數遞增序列的某個數(設為n)的整數倍時,就沒有必要讓i在去

乘遞增素數序列裡的比n大的素數,這又可以等價於 i乘比n大的合適的素數(設為max)可以等於比i大的整數(設

為j)乘比n小的素數(設為min),且這個j不是m的整數倍,即i*max=j*min;
又可以等價與j=i*max/min是一個不是min倍數的整數,根據以前做因式分解的經驗一個整數必然能分解為一個遞增

的素數序列的積,如果我們假設i*max是這麼一個整數,max是因數遞增序列中稍大的素數,則min只要是遞增序列中

小於max的素數,就能使j為整數,很顯然min包含於所有小於max的素數中,自然j是個整數,
又由於i不是max的倍數,max又不是min的倍數(如果是,max是素數嗎?)那麼i必然是min的倍數,又i是第一次成

為所挑選出來的正確的素數遞增序列的某個數(設為n)的整數倍,i必然不是min平方的倍數,即i/min不是min的倍

數,i/min*max也不是min的倍數
至此就證明了if (i%p[j] == 0) break;這一句新增進去是合理的