這是兩種簡單的素數篩法, 好不容易理解了以後寫篇部落格加深下記憶
首先, 這兩種演算法用於解決的問題是 :
求小於n的所有素數 ( 個數 )
比如 這道題
在不瞭解這兩個素數篩演算法的同學, 可能會這麼寫一個isPrime, 然後遍歷每一個數, 挨個判斷 :
- 從2判斷到n-1
bool isPrime(int n) {
for (int i = 2; i < n; i++)
if (n % i == 0)
return false;
return true;
}
- 從2判斷到\(\sqrt{n}\)
bool isPrime(int n) {
for (int i = 2; i <= sqrt(n); i++)
if (n % i == 0)
return false;
return true;
}
然後再加上個count函式 :
int countPrimes2(int n) {
int cnt = 0;
for(int i = 2; i < n; ++i) {
if(isPrime(i)) cnt++;
}
return cnt;
}
這兩種演算法, 一個時間複雜度$ O(n^2) $ , 另一個 $ O(n \sqrt{n}) $ , 但凡出現這樣型別的題, 這麼寫一般都超時了
下面介紹第一種演算法
Eratosthenes 篩法 (厄拉多塞篩法)
核心思想 : 對於每一個素數, 它的倍數必定不是素數
我們通過直接標記, 可以大大減少操作量
比如從2開始遍歷, 則4, 6, 8, 10, 12, 14....都不是素數,
然後是3, 則3, 6, 9, 12, 15.....都不是素數
如圖更加直觀 :
我們可以在判斷小於等於數字n的時候增加一個vis陣列, 他的大小等於n, 預設vis初始化為全零, 假設2-n都是素數, 接下來每遇到一個素數i, 把2-n中vis[i的倍數] = 1, 在接下來遇到的時候直接跳過就可以.
其時間複雜度為 : $ O(nloglogn) $
程式碼 :
int countPrimes(int n) {
vector<int> vis(n, 0); // all number : 0 means prime, 1 means not prime
vector<int> prime(n, 0);
int cnt = 0;
for (int i = 2; i < n; ++i) {
if (!vis[i]) { // 如果vis[i]未被標記, 表明i是素數
prime[++cnt] = i;
for (int j = i*2; j < n; j += i) {
vis[j] = 1; // 標記所有i的倍數
}
}
}
return cnt;
}
線性篩作為對Eratosthenes篩的改進, 能更大程度的減少時間複雜度:
O(n)的篩法----線性篩 ( 尤拉篩 )
在講這個篩法之前, 明確一個概念 :
每一個合數 ( 除了1和它本身以外,還能被其他正整數整除 ), 都可以表示成n個素數的乘積
在Eratosthenes篩中, 我們可以發現, 在排除每一個質數的倍數時, 會有很多重複的操作 :
比如30 = 2x3x5, 那麼30將在對2,3,5倍數標記的時候反覆被處理3次
我們可以思考, 是否有一種方法, 可以僅僅將一個合數標記一次, 就可以打到篩選的目的呢 ?
可以設立一種規則 :
一個合數只能被他的最小素數因子篩去
比如30, 雖然他有2,3,5三個素因子, 我們只讓在對2的倍數作標記時標記30
這樣做, 可以保證我們對每一個數都僅僅只操作了一次, 時間複雜度也就變成了喜聞樂見的O(n) !
具體怎樣實現 ?
我們在遍歷2-n的過程中, 對每一個數i , 從當前已經找到的素數集中從2開始列出prime[j], 當什麼時候有:
那麼可以說明i含有prime[j]這個素因數, 也就是說, 對於比目前 i x prime[j]更大的數, prime[j]將不再是它的最小素因數, 因此到此這一輪篩可以停止了.
舉個例子 :
當 i = 9 時 :
我們可以篩出 :
9 x 2 = 18 ---- 然後進行判斷 9%2 != 0, 因此繼續篩
9 x 3 = 27 ---- 然後進行判斷 9%3 ==0, 因此break
假設我們繼續篩 9 x 5, 會發生什麼?
由於9 % 3 == 0了, 則可以知道 9含有3這個素因數, 如果繼續篩9x5, 可以知道5並不是9x5的最小素因數, 因此不應當在這一輪被篩去.
程式碼 :
int countPrimes(int n) {
vector<int> vis(n, 0); // all number : 0 means prime, 1 means not prime
vector<int> prime(n, 0);
int cnt = 0;
for (int i = 2; i < n; ++i) {
if (!vis[i])
prime[cnt++] = i;
for(int j = 0; j < cnt && i * prime[j] < n; ++j)
{
vis[i*prime[j]] = 1;
if (i % prime[j] == 0) // if prime[j] is i*prime[j]'s minimum prime
break;
}
}
return cnt;
}
時間比較
寫了個cpp計時, 來比較一下這幾種演算法的時間吧~
可以看到, Eratosthenes和線性篩的時間稍有區別, 加大資料量會放大這個區別, 但是麻瓜篩\(O(n^2)\) 在n=7000000的時候已經10s+了, 再加大資料量估計得等到明天了
在寫題的這段日子, 每天都在看別人程式碼的臥槽聲中度過
ref :
OI - wiki 篩法 : https://oi-wiki.org/math/sieve/
線性篩法求素數的原理與實現 : https://wenku.baidu.com/view/4881881daaea998fcc220e99.html
leetcode題解 : https://leetcode-cn.com/problems/count-primes/solution/