素數篩
素數篩,顧名思義,是一種把自然數集合[2,n]中的所有素數篩選出來的算法,通常應用於需要素數打表的題目。
常用的素數篩算法有兩種,分別為埃氏篩 O(nloglogn->1e7) 與歐拉篩 O(n->1e8)。
埃氏篩
最朴素的素數篩算法,核心數學原理為素數的倍數為和數。
思路如下:
對於一串數字:2,3,4,5,6,7,8,9,10
找到第一個未被刪除的數字2,從2開始,刪除其后所有的2的倍數:2、3、4、5、6、7、8、9、10
得到剩余的數組:2、3、5、7、9
接着找到下一個未被刪除的數字3,也刪除其后所有的3的倍數:2、3、5、7、9
得到剩余數組:2、3、5、7
接着應該找下一個未被刪除的數字5,但是5大於sqrt(10),所以終止遍歷,不再查找。
最終得到的素數數組為:2、3、5、7
查看代碼/*生成2-100之間的全部素數*/ #include <cstdio> using namespace std; bool issum[100]; int main() { for (int i = 2; i*i <= 100; i ++) if (!issum[i]) for (int j = i*i; j <= 100; j += i) issum[j] = true; return 0; }
代碼詳解:
創建桶排的bool數組,表示下標這個數是否為和數。
開始循環,如果數字是素數,那就刪去后續這個數的倍數;如果是和數就跳過。(和數總能寫成素數的乘積,所以全部的和數都會被刪除,不必擔心跳過和數會遺漏)
當前刪除數從i2開始,是因為對於當前素數i>2,i2之前的和數總能寫成n×i×p0,其中p0為小於i的素數,這就說明i2之前的和數已通過p0刪除,無需重復刪除;而當i=2時,2、3都是素數,i2=4才為第一個要刪除的和數。
共循環sqrt(100)=10次,這是因為任意和數的最大質因子總不大於該和數的開方值,例如100的最大質因子為5,不超過它的開方值10。
歐拉篩
埃氏篩盡管提高了素數打表的效率,但它依然可能導致一個和數被重復遍歷。
例如,對於和數225,由於225大於32,所以它會在i=3的循環里被刪除一次。同時,又因為225大於52,所以它也會在i=5的循環里被刪除一次。
這就降低了素數篩算法的效率,歐拉篩則通過維護一個新的素數數組解決了這個問題。
歐拉篩算法簡化的核心為僅在該和數的最小質因子循環中把該和數刪除一次。
思路如下:
對於一組數字:2、3、4、5、6、7、8、9、10、11、12
首先找到第一個數2是素數,所以放入素數數組:{ 2 }
接着從原數組的2開始,與素數數組里的每一個元素依次相乘,並刪除乘積對應的數,這里僅刪除2×2=4:2、3、4、5、6、7、8、9、10、11、12
接着找到下一個數3,放入素數數組:{ 2、3 }
從原數組的3開始,依次刪除與素數數組元素的乘積:2、3、4、5、6、7、8、9、10、11、12
接着找到下一個數字4,由於4不是素數,所以不放入素數數組,但是仍要與素數數組{ 2、3 }相乘,並刪除乘積:2、3、4、5、6、7、8、9、10、11、12
這里12不應該被4刪去,因為12的最小質因子是2,它應該在某次循環值與2的乘積為12時才能被刪除。
我們假想原數組循環到了6,6不是素數,不放入素數數組,但要與素數數組{ 2、3、5 }相乘,並刪除乘積,12在這時才應該被刪去。
那如何確保一個數僅在它的最小質因子循環中被刪去呢?
在上面的所有循環中,循環到2時,素數數組中僅有{ 2 },原數組中的2能被素數數組中的2整除,它們的乘積4被刪除;
循環到3時,素數數組為{ 2、3 },3不能被2整除,可以被3整除,乘積6、9都被刪除;
循環到4時,素數數組為{ 2、3 },4能被2整除,不能被3整除,乘積8被刪除,乘積12卻沒有被刪除;
假想循環到6時,容易想到素數數組為{ 2、3、5 },6不能被2、3、5整除,我們結合上述規律,猜測乘積12、18、30均會被刪除;
我們可以直觀地總結出當前值刪除乘積的終止條件:當原數組中的值a能被素數數組中的某個值p整除,就終止遍歷素數數組。
下面是數學證明:
對於找到的值a,如果滿足p0|a且a≠p0,那么對於p0之后的素數p>p0,總有a×p=n×p×p0,由於p0小於p,並且p0是第一個找到的能整除a的質因數,所以p0就是a的最小質因數,則應該在p0的循環中刪去a;
如果p0|a且a=p0,那么p0一定為當前素數數組的尾元素,所以遍歷也會在p0處結束。
總結一下,相比與埃氏篩,歐拉篩多維護了一個素數數組,無論循環到的值是否為素數,都要與素數數組相乘並刪去乘積,並在滿足上述終止條件時停止刪除進入下一個循環,如果當前循環到的值為素數就放入素數數組。
查看代碼/*生成2-100之間的全部素數*/ #include <cstdio> #include <vector> using namespace std; bool issum[100]; vector<int> prime; int main(int argc, char* argv[]) { prime.resize(0); for (int i = 2; i <= 100; i ++) { if (!issum[i]) prime.emplace_back(i); for (int j = 0; i*prime[j] <= 100 && j < prime.size(); j ++) { // 防止乘積越界 issum[i*prime[j]] = true; if(!(i%prime[j])) break; } } return 0; }