1. 程式人生 > 實用技巧 >淺談質數篩法

淺談質數篩法

質數的定義

如果一個整數只有 \(1\) 和它本身這兩個因數,那麼這個數就是一個質數。

同時,如果一個整數 \(>1\) 且它不是質數,那麼我們稱這個數是合數。

\(0,1\) 這兩個數既不是質數,也不是合數。

1. 試除法

如果給定一個數,判斷它是不是質數,那麼只判斷 \([2,n-1]\) 中的任意一個是否能整除它,如果可以那就是合數,否則是質數。

這裡有一個優化:如果 \(n\) 是一個合數,那麼 \(n\) 一定包含一個非 \(1\)\(<\sqrt{n}\) 的因數。

證明可以用反證法:如果 \(n\) 是一個合數,且 \(n=p\times q(p,q>\sqrt{n})\)

,因為有 \((\sqrt{n})^2=n\) ,所以 \(p\times q>n\) ,與已知條件矛盾。

程式碼如下:

inline bool isprime(int n) {
    if (n == 0 || n == 1) return false; //這裡記得特判
    for (int i = 2;i * i <= n; i++)
        if (n % i == 0) return false;
    return true;
}

判斷一次的時間複雜度是 \(\mathcal{O}(\sqrt{n})\) ,如果要判斷 \([1,n]\) 中有哪一些是質數,那麼時間複雜度是 \(\mathcal{O}(n\sqrt{n})\)

。時間複雜度較高。

埃氏篩法

試除法一個一個判斷太慢了,我們可以考慮這樣做:

對於每一個數 \(x\)\(2x,3x,4x,5x\dots\) 一定都不是質數,可以一遍把全部的都篩掉。

也就是說,對於每一個質數(不用管合數的原因是合數的倍數會被質數篩掉),列舉所有小於 \(n\) 的倍數,那些倍數都不是質數。

跟上面試除法一樣,不需要從 \(2\) 列舉到 \(n\) ,因為每一個合數一定有一個小於 \(\sqrt{n}\) 的質因子,所以列舉到 \(\sqrt{n}\) 就可以了。

根據這個思路,寫出程式碼:

bool isprime[N];
inline void Prime(int n) {
	memset(isprime ,true ,sizeof(isprime));
    isprime[0] = isprime[1] = false;
    for (int i = 2;i * i <= n; i++)
        if (isprime[i])
            for (int j = 2;j * i <= n; j++)
                isprime[i * j] = false;
}

時間複雜度為 \(\mathcal{O}(\dfrac{n}{2}+\dfrac{n}{3}+\dfrac{n}{5}+\dfrac{7}{2}+\cdots)=\mathcal{O}(n\log\log n)\)

線性篩/尤拉篩:

埃氏篩還可以優化:有一些數被多次標記了,比如 \(12\) ,在列舉 \(3\) 的倍數的時候算了一次,在列舉 \(2\) 的倍數的時候也算了一次。

線性篩/尤拉篩優化的思路就是:每次只列舉 \(i\) 的質數倍,同時每一個數字只能被它最小的質因子篩一次。

程式碼如下:

int prime[N] ,cnt; bool isprime[N];
inline void Prime(int n) {
    memset(isprime ,true ,sizeof(isprime));
    for (int i = 2;i <= n; i++) {
        if (isprime[i]) prime[++cnt] = i;
        //注意這裡沒有限制只能篩質數的倍數
        for (int j = 1;j <= cnt && prime[j] * i <= n; j++) {
            isprime[prime[j] * i] = false;
            if (i % prime[j] == 0) break;
        }
    }
}

因為每個數字只會被篩一次,所以時間複雜度是 \(\mathcal{O}(n)\)