質數篩算法詳解


今天給大家講解質數篩這個算法。

更好的閱讀體驗

在信息競賽中,我們總是會遇到很多判斷質數的題目,那么在這里就由我來給大家講解一下質數篩算法(這里所有講的算法都是基於篩出從 \(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)\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM