1. 程式人生 > >線性篩素數詳細整理

線性篩素數詳細整理

通過 大於 不可 nlogn 調和級數 () 都是 保存信息 整體

如果你在1個月前讓我判斷素數,我一定會猛敲出以下代碼:

bool check( int num )  
{  
     int tmp =sqrt( num);  
     for(int i= 2;i <=tmp; i++)  
        if(num %i== 0)  
          return 0 ;  
     return 1 ;  //實在是太慢了!
}  

$ $

下面給大家帶來3種篩選素數和一種直接判斷素數

$ $


$ $

什麽是線性篩?

  • 對於求多個質數時與其一個個判斷不如用排除法,用空間換取大量時間。

$ $


$ $
$ $

一般篩法(埃拉托斯特尼篩法):

基本思想

素數的倍數一定不是素數

實現方法

用一個長度為\(N+1\)的數組保存信息(\(0\)表示素數,\(1\)表示非素數),先假設所有的數都是素數(初始化為\(0\)),從第一個素數\(2\)開始,把\(2\)的倍數都標記為非素數(置為\(1\)),一直到大於\(N\);然後進行下一趟,找到\(2\)後面的下一個素數\(3\),進行同樣的處理,直到最後,數組中依然為0的數即為素數

說明:整數\(1\)特殊處理即可。

舉個例子

我們篩前\(20\)個數

首先初始為(\(0\)代表不是素數,\(1\)代表是素數)

\(0\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\)

\(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\) \(1\)

然後從\(2\)開始我們發現\(2\)被標記為素數,我們把\(2\)的倍數全部篩掉

變為:

\(0\) \(1\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\)

接著到\(3\)我們發現\(3\)仍然被標記,把\(3\)的倍數全部篩掉

變為:

\(0\) \(1\) \(1\) \(0\) \(1\) \(0\)

\(1\) \(0\) \(0\) \(0\) \(1\) \(0\) \(1\) \(0\) \(0\) \(0\) \(1\) \(0\) \(1\) \(0\)

接著一直重復下去就得到了最後的素數表:

\(0\) \(1\) \(1\) \(0\) \(1\) \(0\) \(1\) \(0\) \(0\) \(0\) \(1\) \(0\) \(1\) \(0\) \(0\) \(0\) \(1\) \(0\) \(1\) \(0\)

\(2\) \(3\) \(5\) \(7\) \(11\) \(13\) \(17\) \(19\)

Code

const int MAXN = 1000000;  
void get_list()  
{  
    int i, j;  
    for (i=0; i<MAXN; i++) prime[i] = 1;  
    prime[0] = prime[1] = 0;  
    for (i=2; i<MAXN; i++)  
    {  
        if (!prime[i]) continue;  
        for (j=i*2; j<MAXN; j+=i) prime[ j ] = 0;  
    }  
}//調和級數證明可得復雜度為(nlogn),所以不能稱之為線性篩,但是它的實際運行速度也不是特別慢~~

$ $


$ $

$ $

下面我們來介紹一波真正的線性篩(歐拉篩法):

我們發現在上面的篩法中有的數字是多個素數的倍數,也就是說它可能會被重復計算多次,比如說\(6\)同時是\(2\)\(3\)的倍數,它在計算時就被訪問了兩次,這樣會導致效率低下,所以在下面的算法中我們考慮如何優化這種情況。

基本思想

每一個合數可以被唯一地表示成它的一個最小質因子和另外一個數的乘積。

一個合數(\(x\))與一個質數(\(y\))的乘積可表示成一個更大的合數(\(Z\))與一個更小的質數(\(a\))的乘積,那樣我們到每一個數,都處理一次,這樣處理的次數是很少的,因此可以在線性時間內得到解。

實現方法

我們可以從2開始通過乘積篩掉所有的合數。將所有合數標記,保證不被重復篩除。

舉個例子

仍然按上面的例子模擬(這裏\(0\)為是素數,\(1\)為非素數,\(p\)為記錄的素數表):

初始:

\(1\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)

\(p(empty)\)

然後到\(2\)的位置,把\(2\)放入素數表,做當前範圍內可以篩掉的處理(具體是怎樣的看代碼叭):

\(1\) \(0\) \(0\) \(1\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(p\) \(2\)\(3\),把\(3\)放入素數表,繼續處理

\(1\) \(0\) \(0\) \(1\) \(0\) \(1\) \(0\) \(0\) \(1\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)

\(p\) \(2\) \(3\) 然後到了\(4\),它不是個素數,也處理一下

\(1\) \(0\) \(0\) \(1\) \(0\) \(1\) \(0\) \(1\) \(1\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)

\(p\) \(2\) \(3\) .......

然後一直搞下去,最後也能得到完整的素數表,這樣雖然看起來復雜一些,但是實際上我們發現對於每個數的處理幾乎是\(O(1)\)的。

Code:

 void get_list(){
       for(int i=2;i<=maxn;i++){
             if(!is_not_pr[i]) prime[++tot]=i;
             for(int j=1;j<=tot&&i*prime[j]<=maxn;j++){
                   is_not_pr[i*prime[j]]=1;//合數標為1,同時,prime[j]是合數i*prime[j]的最小素因子
                   if(i%prime[j]==0) break;//即比一個合數大的質數和該合數的乘積可用一個更大的合數和比其小的質數相乘得到
             }
       }
}

最難理解的是這句話:

if (i % prime[j] == 0) break;
要理解這句話,(順便不嚴謹地)證明這個算法的時間復雜度和正確性,要從下面兩個方面:

  • 每個數至少被訪問一次
  • 每個數至多被訪問一次
  • 每個數至少被訪問一次

  • 對於質數,一定會在i的循環中訪問到,並確定為質數。
  • 對於質數,一定會在i的循環中訪問到,並確定為質數。
  • 對於合數,一定可以分解為一個最小素因子和其他數的乘積。

比如

合數 \(i = p\)(最小素因子)\(* a\);
\(i%prime[k] ==0\);
\(p * a * prime[k+1]\) 可以被後面的 \(a * prime[k+1]\) 再乘以素數 \(p\) 篩選出來,
(顯而\(p<prime[k+1]\)) 所以\(i%prime[k] == 0\) 時要停止。
證畢
綜上所述,每個數被訪問一次且僅訪問一次!因此整個算法的復雜度是O(n)的。

$ $


$ $

$ $

擴展:米勒羅賓算法

如果我們做題的時候空間不夠怎麽辦?沒辦法篩素數了怎麽辦?沒事,交給你一個辦法!

原理

大於等於5的質數一定和6的倍數相鄰

證明:

\(x≥1\),將大於等於\(5\)的自然數表示如下: ······$6x-1,6x,6x+1,6x+2,6x+3,6x+4,6x+5,6(x+1),6(x+1)+1 ······ $可以看到,不在6的倍數兩側,即\(6x\)兩側的數為\(6x+2,6x+3,6x+4,由於2(3x+1),3(2x+1),2(3x+2),\)所以它們一定不是素數,再除去\(6x\)本身,顯然,素數要出現只可能出現在\(6x\)的相鄰兩側。這裏要註意的一點是,在\(6\)的倍數相鄰兩側並不是一定就是質數。 根據以上規律,判斷質數可以\(6\)個為單元快進,即將直觀判斷法的循環中\(i++\)步長加大為\(6\),加快判斷速度。

原因是,假如要判定的數為\(n\),則\(n\)必定是\(6x-1\)\(6x+1\)的形式,對於循環中\(6i-1,6i,6i+1,6i+2,6i+3,6i+4,\)其中如果\(n\)能被\(6i,6i+2,6i+4\)整除,則\(n\)至少得是一個偶數,但是\(6x-1\)\(6x+1\)的形式明顯是一個奇數,故不成立;另外,如果\(n\)能被\(6i+3\)整除,則\(n\)至少能被\(3\)整除,但是\(6x\)能被\(3\)整除,故\(6x-1\)\(6x+1\)(即\(n\))不可能被\(3\)整除,故不成立。綜上,循環中只需要考慮\(6i-1\)\(6i+1\)的情況,即循環的步長可以定為\(6\),每次判斷循環變量\(k\)\(k+2\)的情況即可,理論上講整體速度應該會是直觀判斷法改進的3倍。

米勒羅賓單次復雜度約為\(log(n)*k\)(\(k\)為常數且一般取\(3\)) 判斷的素數在\(10\)億以內進行\(50w\)次計算也不會超時

Code:

bool check(int a){
    if(a==1) return 0;
    if(a==2||a==3) return 1;
    if(a%6!=1&&a%6!=5) return 0;
    int temp=sqrt(a);
    for(int i=5;i<=temp;i+=6)
    {
        if(a%i==0||a%(i+2)==0) return 0;
    }
    return 1;
}

線性篩素數詳細整理