在這里提供三種線性篩的講解,它們分別是:素數篩,歐拉篩和莫比烏斯篩。
·篩法正確性的重要理論依據:
上述函數均為積性函數。積性函數的性質為:若f(x)是一個積性函數,那么對於任意素數a,b,滿足f(ab)=f(a)*f(b)
·一些可愛的要點(有助於理解篩法原理):
①歐拉篩和莫比烏斯篩是以素數篩為基礎的。
②三者在代碼實現上幾乎是同一框架。
③歐拉函數和莫比烏斯函數的定義介紹:
(1)歐拉函數Phi(x)表示小於等於x的正整數中和x互質的數的個數(注意,1與任何數互質)。
(2)莫比烏斯函數Mob(x)僅有三種值:0,-1,1————如果x能夠被一個大於0的整數的平方整除,那么函數值為0;如果x擁有奇數種 質因數,那么函數值為-1,有偶數種質因數,那么函數值為1(莫比烏斯函數的神奇定義決定了它應用於容斥問題......)。
·線性篩的兩大步驟:
(1)獲得范圍內的素數
線性篩的線性體現在盡可能的使每個數只被“篩”一次。其思想眾所周知地基於: a為任何數,b為質數,那么a*b就不是質數。
在程序實現中體現為,對於當前枚舉的數i,使用所有小於等於i的素數p,去“篩除”數(p * i),即將布爾數組對應位置設為1(表示不為素數)。
值得注意的是,這里會出現三個篩法都會 涉及的關鍵語句:“if(i%Prime[j]==0)break;”。這是一個很簡單有效的優化,原理依舊是基於“盡量只篩一次”。
下面對這個優化做出解釋:在這里我們提出“最小質因子”的概念,然后我們規定每個合數都只被它的最小質因子篩出————這個定義很好地契合了“盡可能只篩一次”的想法。
回到問題,當前如果出現"i%Prime[j]==0"的情況,由於枚舉素數Prime[j]是從小到大的,那么可以說明Prime[j]是i的最小質因子。
此時如果不終止循環,那么Prime[j+1],Prime[j+2]...會與i相乘得到結果然后篩掉這個結果,但是我們從式子中看出,得到的結果的最小質因子是Prime[j],然而我們使用了j之后的素數篩掉了它,不符合開始的原則,這樣做 會使得一個數被篩多次,降低了效率。舉例說明則是:20應該由2 * 10篩掉,而不是被4 * 5篩掉(素數2<素數5)。
然后這里就給出素數篩代碼吧吧吧:
素數的線性篩
(2)正式開篩
(一)歐拉函數的線性篩
首先給出歐拉函數的定義式子:(p表示x的質因數) 
對正整數n,歐拉函數是小於n的正整數中與n互質的數的數目(φ(1)=1)
然后考慮如何用類似於篩素數的方法篩出歐拉函數。
類似於素數,可以做出如下分類討論:(設p為素數)
①Phi(p)=p-1
②若已知Phi(x),且p能整除x: Phi(x*p)=Phi(x)*p
③若已知Phi(x),且p不能整除x:Phi(x*p)=Phi(x)*(p-1)
簡單地證明上述式子:對於①,由於p本身是一個素數,那么比它小的所有數都和它互質,因此答案為p-1。對於②,p已經是x的質因子,因此我們看看歐拉函數的定義式可以的出括號相乘部分不會改變,只會把最前面的x變成x*p。對於③,p是新加入的質因子,相當於式子前面多乘了個p,然后多加了一個括號,所以相當於原式多乘了: p*(1-1/p)即(p-1)。
呆碼在這里:

(二)莫比烏斯函數的線性篩
首先給出歐拉函數的定義式子:
我們在這里使用和上文篩歐拉函數類似的分類討論: (設p為素數) ①μ(p)=-1 ②已知μ(x),且p能夠整除x,則μ(x*p)=0 ③已知μ(x),且p不能夠整除x,則μ(x*p)=-μ(x)三個結論的證明比上文的Euler要簡單很多——只需要照着莫比烏斯函數的定義去討論0,1,-1三種情況就是了。
·細節處理以及高效理解:
(1)堅持"每個數盡量只被篩一次"的原則
這里主要包含兩點。第一,上述篩法依靠這個思想本身就已經很優秀了。第二,也就是上文提到的小優化:
三個篩法都包含了這句話,為什么呢?因為這三種方法其實都是基於素數來進項篩數的,因此既然依靠素數,那么都可以要求每個數只能被最小質因數篩去而不能被其他質因子篩去,這樣做到了每個數只被篩一次。這個細節值得被理解和記住。
(2)找找看,三種篩法的代碼共同之處有哪些?啊,全在下面啦:
小伙伴們問在沒有不熟練的情況下怎么記住模板,其實三種篩法模板是一樣的,所以只需要記住素數模板和歐拉和莫比烏斯函數部分的分類討論就是了。大米餅建議理解所有篩法,這才是最高效的。
·代碼實現:
#include<stdio.h> #define go(i,a,b) for(int i=a;i<=b;i++) using namespace std; struct Sieve { /*void XXX_Sieve() { XXX[1]=1; go(枚舉所有數i) { if(沒有被篩去)記錄素數,XXX[i]=某個值; go(枚舉小於等於i的素數) { if(結果超界)break; 依據分類討論計算XXX[i*prime[j]]的值。 if(i%Prime[j]==0)break; } } }*/ static const int N=200003; bool NotPrime[N]; int Prime[N],t; void Prime_Sieve() { NotPrime[1]=1; go(i,2,200000) { if(!NotPrime[i])Prime[++t]=i; go(j,1,t) { if(1ll*Prime[j]*i>200000)break; NotPrime[Prime[j]*i]=1; if(i%Prime[j]==0)break; } } } int Phi[N]; void Euler_Sieve() { t=0;Phi[1]=1; go(i,2,200000) { if(!NotPrime[i])t++,Phi[i]=i-1; go(j,1,t) { if(1ll*Prime[j]*i>200000)break; Phi[i*Prime[j]]=Phi[i]*(i%Prime[j]?Prime[j]-1:Prime[j]); if(i%Prime[j]==0)break; } } } int Mob[N]; void Mobius_Sieve() { t=0;Mob[1]=1; go(i,2,200000) { if(!NotPrime[i])t++,Mob[i]=-1; go(j,1,t) { if(1ll*Prime[j]*i>200000)break; Mob[i*Prime[j]]=i%Prime[j]?-Mob[i]:0; if(i%Prime[j]==0)break; } } } }Tool; int main() { Tool.Prime_Sieve(); Tool.Euler_Sieve(); Tool.Mobius_Sieve(); return 0; }//Paul_Guderian
大米飄香的總結:
本文介紹了三種篩法,包括素數篩法和兩個積性函數的篩法。線性篩本身並不能成為一道題,但是卻是數論問題中常見的預處理,因此熟練掌握十分重要。如Mobius函數的預處理是解決Mobius-Inversion問題的基礎。在結束了像篩法這樣的重要基礎的學習后,便可信心十足地奔赴曾經認為高深莫測的版塊——數論了。祝你美妙。
每當夜深人靜的時候,望着那燦爛的夜空,
我會感到那里充滿了,太多的夢想…… ——————汪峰《英雄》