篩法求素數
前言
素數(質數):除了1和它本身以外不再有其他因數(能被整除的數)
合數:除了能被1和本身整除外,還能被其他數整除的數
互質:公約數只有1的兩個整數
題目:判斷1-n的范圍內有多少個素數?oj練習
判斷一個數是否為素數,一般會想到以下代碼
//時間復雜度O(n*sqrt(n))
bool sushu(int n){
if(n<=1) return 0;
for(int i=2;i<=sqrt(n);++i){
if(n%2==0) return 0;
}
return 1;
}
當n取很大時,每判斷一個數 i(1<=i<=n)是否為素數,都要枚舉sqrt(i)次,時間復雜度接近O(n^2),
提前建立一張素數表,再通過查表的方式來判斷素數。而這里打表的算法,就需要用到篩法,把表中不是素數的篩去
這里介紹兩種常用的篩法:埃氏篩、歐拉篩(線性篩)
埃氏篩
埃氏篩的思想:素數的倍數都不是素數,如2的倍數(即偶數)都不是素數
用一個prime[n] bool數組來打表,prime[i]=0表示i是素數,prime[i]=1表示i不是素數
一開始都初始化為0,都認為是素數,然后慢慢把不是素數的篩去(prime[i]標為1)
先標記prime[1]=1,因為1不是素數
接着從2開始,把2的所有倍數(合數)都一個個篩去(prime[2*2]、prime[2*3]...)
//時間復雜度O(nloglogn)
#define ll long long
ll n;
bool prime[1000005]; //一開始都認為是質數(初始化為0),0代表是質數,1代表不是
void ai_shai(){
prime[1]=1; //1不是質數
for(ll i=2;i<=n;++i)
if(prime[i]==0) //如果是質數
for(ll j=i*i;j<=n;j+=i) //從i*i開始篩選 因為2*i~(i-1)*i 之前已被2~(i-1)篩出來了
prime[j]=1; //質數的倍數都不是質數
}
上述的從i*i開始篩選可以理解為,比如輪到3的時候,可以從3*3開始篩選,因為3*2已經被2*3篩去了
還有為什么要判斷是否為素數后再進行篩選,比如4不是素數,4的倍數8、16等之前已經被2給篩過了2*4、2*8,就會產生多余步驟
歐拉篩(線性篩)
埃氏篩的時間復雜度為O(nloglogn),如何更進一步的優化?
仔細觀察可發現,上面的埃氏篩也有一些重復計算,有些數會被不同的素數篩多次,比如30=3*10,30=5*6等
所以歐拉篩的思想就是:在埃氏篩的基礎上,讓每個合數只被其最小的質因數篩一次
這使得歐拉篩的時間復雜度可以達到O(n)的線性復雜度,所以歐拉篩也稱線性篩
建立兩個數組,一個是bool類型的素數表prime[],用來表示prime[i]是不是素數。另一個是int類型用來存儲每個素數的數組zyz[],每個素數都會成為某個合數的最小質因子
代碼如下,注釋詳細:
//時間復雜度O(n)
bool prime[1000005]; //1代表不是素數,0代表是素數
int zyz[1000005]; //合數最小的質因子,也記錄着每一個質數
int cnt=0; //記錄質數的個數
void ol_shai(){
prime[1]=1; //1不是質數
for(int i=2;i<=n;++i){
if(prime[i]==0) //如果是質數
zyz[++cnt]=i; //i是一個素數,存入zyz數組中
for(int j=1;j<=cnt&&i*zyz[j]<=n;++j){ //每個素數對應i倍的合數篩去
prime[i*zyz[j]]=1; //合數i*zyz[j]的最小質因子是zyz[j]
if(i%zyz[j]==0) break;
//如果zyz[j]還是i的最小質因數,后面的合數i*zyz[j+1]會被zyz[j](最小質因數)篩去
}
}
}
其中有關break的語句,這里拿出來再解釋一下
因為zyz[j]是從小到大遍歷的素數集合,如果i%zyz[j]=0,那么zyz[j]一定是i的最小質因數
如果i%zyz[j]=0,這里設一個數k,k*zyz[j]=i,下一個循環的i*zyz[j+1]可以分解為k*zyz[j]*zyz[j+1],其中最小質因數為zyz[j],所以會在i=(k*zyz[j+1])時被zyz[j]篩選出來
舉個例子,i=10,j=1,zyz[1]=2,10%2=0,如果沒有break,而是繼續循環 j=2,zyz[2]=3,i*zyz[2]=10*3=5*2*3=2*15,最小質因數為2(zyz[1]),會在i=15的時候,被zyz[1]=2篩出來
這個關鍵的break保證了一個合數只被其最小質因數篩出,使得O(n)的時間復雜度得以成立