篩法
埃篩
埃拉託斯特尼篩法的縮寫,EraSieve (這個英文其實是為了方便做函式名不要再寫shake了)
它的核心思想其實是當確認了一個數是質數以後,把它的所有倍數打上標記說這玩意不是質數。那現在的問題就只剩下怎麼確定一個數是不是質數了。
原理
我們先這樣想,如果一個數 \(n\) 是合數,那麼一定可以表示為 \(a*b\) , 其中 \(a、b\) 都不能等於 \(1\) 和 \(n\) ,那這樣的話 \(a、b\) 都一定會是小於 \(n\) 的,也就是說如果我們在\(2\sim n\) 遍歷的話,只要 \(n\) 是合數,那麼它一定會在遍歷到它之前被篩掉(相當於是它是某個小於它的數的倍數)。那麼我們只需要從 \(2\) 開始遍歷,記錄每一個數有沒有被篩掉,如果在遍歷的它的時候沒有被篩過,那麼它就一定是質數(相當於是它不能表示為某個小於它的數的倍數,大於它的數就更不可能)。
程式碼
int not_pri[MX]={0};//not_pri[i]表示i這個數是否為質數,質數為0,非質數為1
void EraSieve(int range){//篩出所有小於range的質數
not_pri[1]=0;//如果想讓not_pri表示非合數,那麼這裡需要讓not_pri[1]=1
for(int i=2;i<=range;i++){
if(not_pri[i]==0){//如果i不是質數,那麼它就沒有必要再翻倍,因為它的倍數必然可以表示為它的某個質因數的倍數,所以必定已經被篩過了
for(int j=2;i*j<=range;j++){
not_pri[i*j]=1;//篩除倍數
}
}
}
}
複雜度分析
時間複雜度:\(O(n\log\log n)\) (範圍*質數個數)
尤拉篩
也叫線性篩,EulerSieve (還是方便的函式名)
尤拉篩其實是針對埃篩的超級優化,它們的大致想法是一樣的,都是把質數的倍數打上標記來篩掉合數,埃篩的花時間主要是在篩掉倍數,我們繼續想一想埃篩的遍歷方式裡面是不是有針對合數的倍數的剪枝,但是我們來看這樣一個例子:
當\(i=2\)的時候,篩掉了\(\ \ 4,\ \ 6,\ \ 8,10,12,14,16,18\dots\)
當\(i=3\)的時候,篩掉了\(\ \ 6,\ \ 9,12,15,18,21,24,27\dots\)
你會發現,雖然我們只篩掉質數的倍數,但是在上面那個例子裡,所有 \(6\) 的倍數都會被篩兩遍,那如果一個數有多個質因子呢?重複篩的次數是不是更多了?是不是就造成了更多的無用迴圈?那麼這個只根據是不是質數進行剪枝是不是可以更加精準呢?所以尤拉篩就誕生了。
讓每一個數只被它最小的質因數篩到,就是尤拉篩改進後的剪枝方法,也是尤拉篩的核心思想。那具體要怎麼實現呢?
原理
我們拿 \(12\) 做一個例子,按照我們埃篩的方式來,會這樣反覆的篩掉 \(12\) : \(2*6=3*4\) 。想辦法只用\(2*6\)來篩掉 \(12\) 就可以了,我們可以發現一個數除以它最小的質因數得到的數一定是它最大的因數(\(2*6\)),而其他的表示方法一定可以分解成更小的質因數相乘的形式(\(3*4=3*2*2=2*6\)),也就是說如果一個數 \(n\) 除以一個質因數 \(x\) 得到的數 \(y\) 是一個比這個質數 \(x\) 更小的質數的倍數,那麼這個質數 \(x\) 就一定不是最小的質數,自然也就不用這個質數來篩了。 結合 \(12\) 的例子來理解一下這句話:\(n=12,x=3\) 得 \(y=12 /3=4\) ,因為\(\ 4=2*2\ , \ 2<3\),所以 \(3\) 不是最小質數。
其實這樣麻煩的判別方法對於單個單個的數來說肯定不是最優的(直接挨個列舉\(2\sim\sqrt{n}\)看是不是整除豈不是更簡單),之所以要用這樣的一個性質,是因為我們需要篩啊,篩就需要列舉倍數,所以需要找一個通過倍數的關係來確定是不是最小質因數的性質。
接著看,可以發現只要是任何一個大於 \(2\) 的質數 \(x\) 來乘上 \(4\) 得到的準備篩掉的數一定可以寫成 \(2*2x\)的形式,就是說它的最小質因數肯定就不是 \(x\) 了,而是 \(2\) 了,那麼就沒有必要繼續將之後的質數都沒有必要篩掉他們的 \(4\) 倍,因為他們的 \(4\) 倍數肯定可以被 \(2\) 篩掉。更一般化的形式就是,如果正在篩掉一個質數\(x\)的\(y\)倍,而這個 \(y\) 能被一個比 \(x\) 小的質數 \(z\) 整除,所以 \(x*y\) 一定可以表示為 \(z*k\) ,而 \(z\) 是比 \(x\) 小的,回到尤拉篩的核心思想:“每一個數只被它最小的質因數篩到”,所以所有大於 \(x\) 的質數都不用篩 \(y\) 倍了,因為一定可以在篩去 \(z\) 的 \(k\) 倍時篩去。(這裡可以求出來\(\displaystyle k=\frac{x*y}{z}\));
為了用上這個那我們再改一下篩的方法,埃篩的方法是通過列舉一次性把這個質數的所有倍數全部標記完,而尤拉篩是先列舉質數的乘數,每一次標記上目前所有已知質數的某個確定的倍數(比如說都篩去目前已所有質數的\(3\)倍,下一次篩去所有已知質數的 \(4\) 倍,每篩一次單個質數最多隻會多一倍),這樣就可以利用到上兩段發現的性質來進行優化了。而這裡的列舉倍數再仔細一看,其實可以利用埃篩裡列舉的 \(i\) ,\(i\) 在這裡有了兩個意義:①當前需要判斷是不是質數的數;②所有質數篩去的倍數。
程式碼
int pri[MX]={0};// pri[i]儲存質數表,其中pri[0]表示這個質數表裡有多少個數
int not_pri[MX]={0};//同埃篩裡的not_pri[]
void EularSieve(int range){
for(int i=2;i<range;i++){
//i的第一個意義:判斷i是不是質數,判斷理由和埃篩一模一樣
if(not_pri[i]==0)
pri[++pri[0]]=i;
//i的第二個意義:篩去 目前所有已知質數*i 得到的合數
for(int j=1;j<=pri[0];j++){//列舉當前已知質數
if(pri[j]*i>range)break;//超出判斷範圍
not_pri[pri[j]*i]=1;
//重點!
if(i%pri[j]==0)break;
//用上面一般化的形式來表述的話,x=pri[j],y=i,這裡由於pri是遞增的,所以就相當於是判斷y能不能被更小的質因數整除
//如果i%pri[i]為0了,那麼就找到了一個比x小的質數z,那麼之後的倍數的最小的質因數就不再是x而是z了,所以不用接著篩了
}
}
}
複雜度分析
時間複雜度:\(O(n)\) (每個數只被篩去一次)