篩法求素數

當只需要判斷某個數是不是素數的時候,我們可以直接通過素數的定義來求,但是如果要求某個範圍內的素數的個數的時候這個方法就不太合適了。雖然我們可以進行預處理,但是這種方法比較慢,一旦範圍過大,預處理過程便會超時。這時候就引入了一種新的簡單檢定素數的方法-----埃拉託斯特尼篩法

埃拉託斯特尼篩法

從2開始,將每個素數的各個倍數(一倍除外),標記成合數。一個素數的各個倍數,是一個差爲此素數本身的等差數列。此爲這個篩法和試除法不同的關鍵之處,後者是以素數來測試每個待測數能否被整除。埃拉託斯特尼篩法(英語:sieve of Eratosthenes ),簡稱埃氏篩,也稱素數篩。這是一種簡單且歷史悠久的篩法,用來找出一定範圍內所有的素數。埃拉託斯特尼篩法是列出所有小素數最有效的方法之一。

算數基本定理

每個整數 n >= 2 可唯一分解成素數乘積 n = p1p2…pr.

普通線性篩法

由算數基本定理可知,任何一個大於等於 2 的數,都可以表示成兩個數相乘的形式,只有 1 和 它本身這一對組合的是素數,剩下的都是合數。對於一個合數 a,則一定等於某個素數 b 與另一個素數或者合數 c 相乘。也就是換句話說,通過篩掉所有素數的倍數,就可以將所有的合數篩掉而只剩下素數,於是有了如下的普通線性篩法。
#include <stdio.h>
#include <string.h>

#define MAX (int)1e5  //待判斷素數範圍0~MAX
#define ll long long
 
ll prime[MAX + 1], cnt;                                                              
bool isPrime[MAX + 1];

void init_prime()
{
    cnt = 1;
    memset(isPrime, 1, sizeof(isPrime));
    isPrime[0] = isPrime[1] = 0;
    
    for (ll i = 2; i <= MAX; i++) {
        if (!isPrime[i]) continue;//跳過合數
        prime[cnt++] = i;//保存素數
        for (ll j = 2 * i; j <= MAX; j += i)
            isPrime[j] = 0;//素數倍數都不是素數
    }
}

int main()
{   
    init_prime();
    for (ll i = 1; i < cnt; i++)
        printf("%lld%s", prime[i], i == cnt - 1 ? "\n" : " ");
    return 0;
}

快速篩法

再次思考普通線性篩法,一個合數 n = p1p2…pr.(pi都是素數,1 <= i <= r>),是p1的倍數,也是p2的倍數,……也是pr的倍數,換句話說,該合數被重複篩了 r 次。顯然,最好的結果是每個合數只被篩一次,算法是線性的時間。那麼該如何去做呢?

一種解決辦法是約定每次只在 pmin 處,對該合數 n 做篩除。

#include <stdio.h>
#include <string.h>

#define MAX (int)1e2
#define ll long long
 
ll prime[MAX + 1], cnt;                                                              
bool isPrime[MAX + 1];

void init_prime()
{
    cnt = 1;
    memset(isPrime, 1, sizeof(isPrime));
    isPrime[0] = isPrime[1] = 0;

    for (ll i = 2; i <= MAX; i++) {
        if (isPrime[i]) 
            prime[cnt++] = i;  //位置1
        for (ll j = 1; j < cnt && i * prime[j] <= MAX; j++) {
            isPrime[i * prime[j]] = 0; //位置2
            if (!(i % prime[j])) break; //位置3
        }
    }
}

int main()
{   
    init_prime();
    for (ll i = 1; i < cnt; i++)
        printf("%lld%s", prime[i], i == cnt - 1 ? "\n" : " ");
    return 0;
}
這個算法可以保證每個數字都只被標記一遍,實現方法如下:

對於所有大於1的數,分爲素數和合數,而合數又分爲:

  1. 一個素數乘以一個素數
  2. 一個素數乘以一個合數

在位置 1,得到了所有素數,在位置 2 標記了所以合數。
在位置 2 的時候,又分爲兩種可能:

  1. i 是素數,prime[j] 是素數
  2. i 是合數,prime[j] 是素數

如上解釋了爲何可以標記合數

那麼是如何不重複標記合數的呢?

對於 n = pmin * c, 更具最小約定應該有 c >= pmin,反過來即是每次拎一個數 c’,只需要到小於等於它的素數範圍做乘法篩除即可。可是我們發現,依然會出現 34 = 26 這樣的重複篩除,該如何避免這樣的重複呢?依靠位置 3。

  1. 對於 i 是素數的情況,這個和下一個素數 prime[j + 1] 繼續標記肯定不會重複;

  2. 對於 i 是合數的情況,如果這個合數分解質因數後:

    1. 最小的質因數等於 prime[j], 則若繼續與下一個素數 prime[j + 1]相乘標記合數,就違反了不重複的約定-----因爲對於由 i * prime[j + 1] 標記的合數,顯然有更小的素數 prime[j] 去做乘積來標記。所以這種情況就應該停止迭代。

    2. 最小質因數大於 prime[j],則就可以繼續與下一個素數相乘繼續標記,下一輪迭代不會出現重複標記的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章