Part 0:概念
先給幾個概念(很重要):
- 合數:如果\(xy=z\text{且}x,y\text{為正整數}\),我們就說\(x,y\text{是}z\text{的合數}\)
- 素數:如果數\(a\)的合數只有\(1,a\),則\(a\)就是一個素數
- 整除:整數\(b\)除以非零整數\(a\),商為整數,且余數為零, 我們就說\(b\)能被\(a\)整除,記做\(a | b\)。數學中,求一個數的余數的運算叫做取余,用\(a MOD b\)表示求a除以b的余數,計算機中用
%
當然,如果有\(a | b\),那么我們可以寫成\(a MOD b = 0\) - 不含0,1的所有自然數除了素數就是合數
Part 1:普通篩法及優化到根號
首先,從普通篩法開始:
#include <iostream>
using namespace std;
int main(){
return 0;
}
判斷一個數是不是素數,就從他的定義入手
定義是啥?回顧一下:
如果數\(a\)的合數只有\(1,a\),則\(a\)就是一個素數
再次回顧合數的概念:
如果\(xy=z\text{且}x,y\text{為正整數}\),我們就說\(x,y\text{是}z\text{的合數}\)
我們知道,不含0的所有自然數不是素數就是合數,而我們算法的名字叫做篩素數,而素數的定義離不開合數
干脆,我們把合數篩掉吧!
我們從\(xy=z入手\)。顯然,\(x,y,z \ neq 0\),可以大膽的除一下:\(x = \frac{z}{y}\)
如果你比較熟悉開頭的那些定義,你就會發現:因為\(x\)是個正整數,所以\(z | y\),也就是\(z MOD y = 0\)
這個地方可以好好理解一下
那么,我們開始填充代碼:
#include <iostream>
using namespace std;
int main(){
bool flag = 1;//素數判斷標簽
int n = 10;//n代表我們需要篩從1~n的素數
for(int i = 2;i <= n;i ++){//很重要!公式中的z(也就是這里的i)要從2開始,想想為什么
//cout << "i = " << i << endl;
for(int j = 2;j < i;j ++){//很重要!想想為什么公式中的y(也就是這里的j)為什么從2開始,為什么要比i小?
//cout << "進入二層循環,j = " << j << endl;
if(i % j == 0){//如果是合數
flag = 0;
//cout << "i % j = " << i%j << endl;
//cout << "二層循環if,i = " << i << "j = " << j << endl;
break;
}
}
if(flag) cout << i << endl;//否則輸出
flag = 1;//小細節~自己模擬一下
}
}
好的,代碼是出來了
可以更快嗎?當然可以
我舉個例子:36。
36的因數有:{1,36},{2,18},{3,12},{4,9},{6,6}
把36的因數排個序:{1,2,3,4,6},{6,9,12,18,36}
以6為分界線,我們可以把它分為了兩份,請注意:
\(\sqrt{36} = 6\)
哇塞!我們只需要循環到\(\sqrt{n}\)我們就可以結束了(因為因數是兩兩對應的(如果不考慮去重(比如49的因數表暫時定為:1,7,7,49)))
改進一下:
#include <iostream>
#include <cmath>
using namespace std;
int main(){
bool flag = 1;//素數判斷標簽
int n = 10;//n代表我們需要篩從1~n的素數
for(int i = 2;i <= sqrt(n);i ++){//很重要!公式中的z(也就是這里的i)要從2開始,想想為什么
cout << "i = " << i << endl;
for(int j = 2;j < i;j ++){//很重要!想想為什么公式中的y(也就是這里的j)為什么從2開始,為什么要比i小?
cout << "進入二層循環,j = " << j << endl;
if(i % j == 0){//如果是合數
flag = 0;
cout << "i % j = " << i%j << endl;
cout << "二層循環if,i = " << i << "j = " << j << endl;
break;
}
}
if(flag) cout << "------" << i << endl;//否則輸出
flag = 1;
}
}
Part 2 真正的篩——埃式篩
剛才那個算法的思想再怎么說,也是個取素數
但是,判斷合數顯然比判斷素數更簡單
那么,為什么不把合數篩走,剩下的不就是素數了嗎?
好辦法!接下來我們想下怎么篩
其實啊,可以發現某一個不為0/1的數 × 另一個不為0/1的數 = 合數
那么,假設有一個數\(q\),我們只需要篩掉\(1q,2q,3q\text{……一直到}iq>n\)時,接着q++,繼續篩!
上代碼:
#include <iostream>
//代碼來自https://www.cnblogs.com/Return-blog/p/12307038.html
using namespace std;
bool book[1000000];//素數標記盒子
int main(){ //求1~100以內的素數
for(int i=2;i<=100;i++)
if(!book[i]){//因為book數組在main()外部自動填充為0,所以需要!0轉換為11
for(int j=i+i;j<=100;j+=i)//q,2q,3q,4q...
book[j]=1;//標記上1的就是合數了!
}
for(int i=2;i<=100;i++)
if(!book[i]) cout << i << endl;//是0就輸出~
return 0;
}
哦太棒了!
但是!請你自己模擬一下這個過程,你會發現個奇怪的過程拖慢了速度!
當i = 2 => 2i=4,3i=6,4i=8...
當i = 3 => 2i=6,3i=9,4i=12...
當i = 4 => 2i=8...
Oh No!又開始重復了!
重復怎么辦?再篩掉!
Part 3:線性篩
//代碼來自https://www.cnblogs.com/Return-blog/p/12307038.html
#include <iostream>
using namespace std;
bool book[20005];
int prime[20005],pos,i,j;
int main(){ //求1~100以內的素數
for(i=2;i<=100;i++){
if(!book[i]) prime[++pos]=i; //判斷是否被篩過。!0(沒有)的話,就記錄一下,
for(j=1;j<=pos;j++){
if(i*prime[j] > 100) break; //范圍:不讓他超過i,超過就break
book[i*prime[j]]=1; //i乘上素數得到的書就標記下,篩過了!
if(i%prime[j]==0) break; //!!!prime數組 中的素數是遞增的,當 i 能整除 prime[j],那么 i*prime[j+1] 這個合數肯定被 prime[j] 乘以某個數篩掉 想想為什么
}
}
for(int i=1;i<=pos;i++)
cout << prime[pos];
return 0;
}
Oh太棒了!你終於搞到了最快的方法
最后送你個福利:證明一下為什么:當 i 能整除 prime[j],那么 i*prime[j+1] 這個合數肯定被 prime[j] 乘以某個數篩掉
首先,prime數組中的素數是遞增的(這個不用說)
因為i中含有prime[j],prime[j]比prime[j+1]小
即\(i=k\times prime[j]\)
那么\(i\times prime[j+1]=(k\times prime[j])\times prime[j+1]\)
也就是\(k’\times prime[j]\)
看不懂沒關系,板子背下來就好了~