今天給大家講解質數篩這個算法。
在信息競賽中,我們總是會遇到很多判斷質數的題目,那么在這里就由我來給大家講解一下質數篩算法(這里所有講的算法都是基於篩出從 \(1\) 到 \(n\) 之間的素數的算法)。
1.普通篩法
最普通的篩法,也就是將前 \(n\) 個正整數一個一個來判斷是否為素數,並且在判斷素數的時候要從 \(2\) 枚舉到 這個數\(-1\) 來判斷。
關鍵代碼
for(int i=1;i<=n;++i)//枚舉1到n
{
bool flg=0;
for(int j=2;j<i;++j)//枚舉2到i
{
if(i%j==0)//如果i%j=0,也就是i已經不為素數了
{
flg=1;//打上標記
break;//跳出循環,不用再枚舉了
}
}
if(flg==0)//如果沒有被打上標記
prime[i]=1;//prime來標記這個數是否為素數。
}
這樣的時間復雜度最劣近似 \(O(n^2)\)。
2.普通篩法的優化
學過奧數的朋友們可能會發現,在判斷素數的時候,不一定需要枚舉到 \(i-1\) 只需要枚舉到 \(\sqrt{i}\) 就可以判斷出來了。
關鍵代碼
for(int i=1;i<=n;++i)//枚舉1到n
{
bool flg=0;
for(int j=2;j*j<=i;++j)//枚舉2到i
{
if(i%j==0)//如果i%j=0,也就是i已經不為素數了
{
flg=1;//打上標記
break;//跳出循環,不用再枚舉了
}
}
if(flg==0)//如果沒有被打上標記
prime[i]=1;//prime來標記這個數是否為素數。
}
這樣的時間復雜度最劣近似 \(O(n\sqrt{n})\)。
3.暴力篩
我們發現,上面兩種篩法會篩到許多沒有意義的數,所以我們必須換一種思想方式。
暴力篩,就是先將 \(prime\) 數組全部賦值為 \(1\)。(記得將 \(prime_1\) 賦值為 \(0\) )。
仍然是要從 \(1\) 枚舉到 \(n\) 。我們先假設當前枚舉到了 \(i\) 。
如果 \(prime_i=1\) 也就是 \(i\) 為質數,則我們可以知道 \(i\) 的倍數均為合數,所以我們就將 \(prime_{i\times k<n ,k>=2}\) 賦值為 \(0\) 。
最終篩完之后,如果 \(prime_i=1\) , \(i\) 就是質數。
關鍵代碼
memset(prime,1,sizeof(prime));
priem[1]=0;
for(int i=2;i<=n;++i)
{
if(prime[i])
{
for(int j=2;j*i<=n;++j)
prime[i*j]=0;
}
}
顯然,該程序一共會執行 \(\sum\limits_{i=2}^n \dfrac{n}{i}\approx \lim\limits _{n \to \infty}\sum\limits_{i=2}^n \dfrac{n}{i}= n \ln n\) 次。
4.埃氏篩
埃氏篩是基於暴力篩法的一種優化。
我們發現,對於暴力篩中小於 \(i\times i\) 的數,假設其為 \(i \times j\),則必然有 \(j<i\),所以這個數已經被 \(j\) 篩掉了,不用再去考慮,所以對於第二重循環,我們可以從 \(i\),一直枚舉到邊界。
memset(prime,1,sizeof(prime));
priem[1]=0;
for(int i=2;i<=n;++i)
{
if(prime[i])
{
for(int j=i;j*i<=n;++j)
prime[i*j]=0;
}
}
對於第一重循環,可以只枚舉到 \(\sqrt n\),因為在這個范圍以內就可以篩掉所有的合數。
對於時間復雜度,因蒟蒻能力有限,不會證明,只給出具體時間復雜度是 \(n\ln \ln n\)。
5.歐拉篩(線性篩)
我們發現,埃氏篩已經很快了,但是還是有所不足。
因為在埃氏篩中,有很多數有可能被篩到很多次(例如 \(6\) , 他就被 \(2\) 和 \(3\) 分別篩了一次)。 所以在歐拉篩中,我們就是在這個問題上面做了優化,使得所有合數只被篩了一次。
首先,我們定義 \(st_i\) 數組表示 \(i\) 是否為質數,\(primes_i\) 儲存已經找到的所有質數,\(cnt\) 儲存當前一共找到了多少質數。
如果當前已經枚舉到了 \(i\) 。如果 \(st_i=1\) ,也就是 \(i\) 為素數。則 \(primes_{cnt+1}=i\)。
然后我們每一次枚舉都要做這個循環: 枚舉 \(j\) 從 \(1\) 到 \(cnt\)。\(st_{primes_j\times i}=0\)(因為 \(primes_j\) 為素數,\(i\) 就表示這個素數的多少倍,要把他篩掉)。
注意,接下來是重點! 如果 \(i\mod primes_j=0\),跳出第二層循環
為什么呢,我們可以這樣想。
我們假設當前枚舉到的 \(i\) 的最小質因子為 \(primes_k\)。
則在枚舉 \(1 \to k-1\) 時,可以保證 \(primes_j<primes_k\)。(\(primes\) 數組一定遞增。)所以 \(i\times primes_j\) 的最小質因子一定是 \(primes_j\)。
當枚舉到了 \(k\) 時,可以發現,當前的 \(primes_k\times i\) 的最小質因子一定是 \(primes_k\),只不過多含了幾個 \(primes_k\)。
最后枚舉 \(k\) 以后的數時,我們可以發現當前 \(primes_j>primes_k\),這個素數並沒有被他的最小質因子篩掉,所以 \(break\)。
關鍵代碼
memset(st,0,sizeof(st));
st[1]=0;
for(i=2;i<=n;i++)
{
if(st[i])
primes[cnt++]=i;
for(j=0;primes[j]*i<=n&&j<=cnt;j++)
{
st[primes[j]*i]=0;
if(i%primes[j]==0)
break;
}
}
關於正確性
你可能會問,為啥他一定能把所有的素數篩到?
假設有質數沒有篩到,其中一個為 \(z\)。
設其最小質因子為 \(primes_l\)。
則當我們枚舉到 \(z/primes_l\) 時,他的循環邊界條件一定能枚舉到 \(l\),所以如果他沒有枚舉到一定是中間 \(break\) 了。
假設使他 \(break\) 的質數為 \(primes_x\) 且 \(x<l\)。則 \(z/primes_l \mod primes_x=0\) 且 \(primes_x<primes_l\)。
而這樣的話,\(z\) 的最小質因子就是 \(primes_x\) 而不是 \(primes_L\),所以矛盾。
所以這樣的方法一定是正確的,且一定使用到的最小質因子來篩。
這樣的時間復雜度為 \(O(n)\)。
