【數論】素數篩法

Jefferyzzzz發表於2024-06-02

素數篩法

一.前言

素數是數論中頗為重要的一類數,我們往往需要對其進行判斷。當我們需要尋找一個大範圍內的所有素數時,如何篩選這些素數就顯得非常重要了。本文中介紹了三種篩法:樸素判定,埃拉托色尼篩法,尤拉篩法。

二.樸素判定法

當我們對一整個範圍內的素數進行篩選時,一種樸素的方法很容易想到,那就是對其中每個素數進行判定,最簡單的方法就是用試除法,對所有\(<=\sqrt{x}\)的數依次試除,之所以不繼續往後,因為乘法交換律,後面的因子都會與前面的因子結合,如果前面判定無因子,後續也不可能再有了。以下是程式碼實現。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;
    scanf("%d",&n);
    for (int i = 2; i <= n; ++i)
    {
        bool isPrime = true;
        for (int j = 2; j * j <= i; ++j)
        {
            if (i%j==0)
            {
                isPrime = false;
                break;
            }

        }
        if(isPrime) printf("%d ",i);
    }
    return 0;
}

每個數\(i\)都需要判定\(\sqrt{i}\)次,篩選\(n\)個數,平均時間複雜度為\(\Theta(n\sqrt{n})\),。明顯低效,缺點很明顯他樸素的對每個數進行素性判定。而我們在我們在尋找一個範圍內所有素數時,由於合數都能被兩個小於他的因子相乘,我們是不是能透過這種性質篩去所有合數,而得到剩餘的素數呢?

三.埃拉托色尼篩法(埃氏篩)

眾所周知,一個素數的倍數是素數,每個合數都能分解成若干個質因數,我們可以藉此性質篩去每個素數的倍數,這樣留下的必定是素數,因為比該素數小的所有可能因子的倍數已經被篩去。以下是實現程式碼。

#include<bits/stdc++.h>

using namespace std;
const int N = 1e7;
int isNotPrime[N];

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 2; i <= n; ++i) {
        if (!isNotPrime[i]) {
            printf("%d ", i);
            for (int j = i * i; j <= n; j += i) {
                isNotPrime[j] = 1;
            }
        }
    }
    return 0;
}

這裡有幾個魔鬼細節。首先我們只對素數進行倍數篩去,因為一個合數必定有小於他的因子,那麼他的倍數一定在小於他的因子時篩去,沒有必要重複篩。另外內層迴圈\(j\)是從\(i^2\)開始的,因為\(j\)\(1\rightarrow j-1\)倍已經在篩去\(1 \rightarrow j-1\)的倍數時篩去。每個素數都會執行\(n/2+n/3+n/5...n/k(k為素數)\)次篩去其數值約等於\(n\log_2 \log_2 n\)次,所以埃氏篩時間複雜度約等於\(\Theta (n\log_2 \log_2 n)\)。已經十分逼近線性,他無法達到線性的原因是他出現了重複篩去的問題,例如在對\(5\)進行倍數篩去時,篩去了\(40,50,80...\),而這些數在\(2\)的倍數時已經篩去,這是埃氏篩法無法避免的缺點。

四.尤拉篩(線性篩)

埃氏篩最大的缺點就是會對含有同一個因子的數重複篩去,這會浪費大量時間。尤拉篩,又稱歐式篩,他運用和數能拆分成以他的最小素因數與另一個在篩選範圍內的數的乘積形式,這一思想,掃描過範圍內每一個數,在篩選的同時統計素數,每次篩去已經發現的素數中的素數與每掃描到的數的乘積,當發現掃描到的數與已發現的素數非互素,那麼說明掃描到的數的所有倍數均含有這個素數因子,所以無需繼續向後與其他素數做乘積,因為那個較小的素數的倍數必定會包含掃描數與後續素數的乘積(因為較小的素數是掃描數的因子)。這可以完美的避免重複篩選,因為他只會篩選到掃描到的數與其最小素因數的乘積,這不會導致某一個重複的因子在一次掃描中被重複利用,也不會導致某一個數被重複篩去,因為他會且僅會被他的最小因子所篩去,而不會被該因子的倍數所篩去

#include<bits/stdc++.h>

using namespace std;
const int N = 1e7;
int isNotPrime[N];
int Prime[N], cnt;

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 2; i <= n; ++i) {
        if (!isNotPrime[i]) {
            printf("%d ", i);
            Prime[++cnt] = i;
        }
        for (int j = 1; j <= cnt; ++j) {
            if (Prime[j] * i > n) break;\\如果超過範圍結束。
            isNotPrime[Prime[j] * i] = 1;\\篩去當前掃描數與已發現的素數構成的合數,合數的最小數因子必定是Prime[j],因為素數是按序掃描的。
            if (i % Prime[j] == 0) break;\\該素數是掃描數的因子,那麼掃描數與後續素數的乘積必定含有該素因子,後續素數與掃描數的乘積的最小因子必定不是後續素數,而是該素數。
        }
    }
    return 0;
}

尤拉篩對於\(n\)範圍每個數掃描一次,在掃描過程中對每個合數進行一次篩去。每個數都會被掃描一次,篩一次,時間複雜度是完全線性的!為\(\Theta(n)\)

五.總結

素數篩法可以在快速的時間內預處理一段範圍內的素數,對於小範圍需要高頻的素數的素數判定的問題起到重要問題。而低頻且大範圍的素數判定問題,則需要高效的素性測試演算法,我會單獨出一篇文章,敬請期待。

相關文章