線性篩素數 - 歐拉篩 (包含正確性和復雜度的證明)


2019-04-02 更新

想要快速地篩出一定上限內的素數?

下面這種方法可以保證范圍內的每個合數都被刪掉(在 bool 數組里面標記為非素數),而且任一合數只被:

“最小質因數 × 最大因數(非自己) = 這個合數”

的途徑刪掉。由於每個數只被篩一次,時間復雜度為 \(O(n)\)

歐拉篩

先瀏覽如何實現再講其中的原理。


實現

bool isPrime[1000001];
//isPrime[i] == 1表示:i是素數
int Prime[1000001], cnt = 0;
//Prime存質數

void GetPrime(int n)//篩到n
{
	memset(isPrime, 1, sizeof(isPrime));
	//以“每個數都是素數”為初始狀態,逐個刪去
	isPrime[1] = 0;//1不是素數
	
	for(int i = 2; i <= n; i++)
	{
		if(isPrime[i])//沒篩掉 
			Prime[++cnt] = i; //i成為下一個素數
			
		for(int j = 1; j <= cnt && i*Prime[j] <= n/*不超上限*/; j++) 
		{
        	//從Prime[1],即最小質數2開始,逐個枚舉已知的質數,並期望Prime[j]是(i*Prime[j])的最小質因數
            //當然,i肯定比Prime[j]大,因為Prime[j]是在i之前得出的
			isPrime[ i*Prime[j] ] = 0;
            
			if(i % Prime[j] == 0)//i中也含有Prime[j]這個因子
				break; //重要步驟。見原理
		}
	}
}


原理概述

代碼中,外層枚舉 \(i = 1 \to n\)。對於一個 \(i\),經過前面的腥風血雨,如果它還沒有被篩掉,就加到質數數組 \(Prime[]\) 中。下一步,是用 \(i\) 來篩掉一波數。

內層從小到大枚舉 \(Prime[j]\)\(i×Prime[j]\) 是嘗試篩掉的某個合數,其中,我們期望 \(Prime[j]\) 是這個合數的最小質因數 (這是線性復雜度的條件,下面叫做“篩條件”)。它是怎么得到保證的?

\(j\) 的循環中,有一句就做到了這一點:

			if(i % Prime[j] == 0)
				break; 

\(j\) 循環到 \(i \mod Prime[j] == 0\)恰好需要停止的理由是:

  • 下面用 \(s(smaller)\) 表示小於 \(j\) 的數,\(L(larger)\) 表示大於 \(j\) 的數。

  • \(i\) 的最小質因數肯定是 \(Prime[j]\)

    (如果 \(i\) 的最小質因數是 \(Prime[s]\) ,那么 \(Prime[s]\) 更早被枚舉到(因為我們從小到大枚舉質數),當時就要break)

    既然 \(i\) 的最小質因數是 \(Prime[j]\),那么 \(i × Prime[j]\) 的最小質因數也是 \(Prime[j]\)。所以,\(j\) 本身是符合“篩條件”的。

  • \(i × Prime[s]\) 的最小質因數確實是 \(Prime[s]\)

    (如果是它的最小質因數是更小的質數 \(Prime[t]\),那么當然 \(Prime[t]\) 更早被枚舉到,當時就要break)

    這說明 \(j\) 之前(用 \(i × Prime[s]\) 的方式去篩合數,使用的是最小質因數)都符合“篩條件”。

  • \(i × Prime[L]\) 的最小質因數一定是 \(Prime[j]\)

    (因為 \(i\) 的最小質因數是 \(Prime[j]\),所以 \(i × Prime[L]\) 也含有 \(Prime[j]\) 這個因數(這是 \(i\) 的功勞),所以其最小質因數也是 \(Prime[j]\)(新的質因數 \(Prime[L]\) 太大了))

    這說明,如果 \(j\) 繼續遞增(將以 \(i × Prime[L]\) 的方式去篩合數,沒有使用最小質因數),是不符合“篩條件”的。

小提示:

\(i\) 還不大的時候,可能會一層內就篩去大量合數,看上去耗時比較大,但是由於保證了篩去的合數日后將不會再被篩(總共只篩一次),復雜度是線性的。到 \(i\) 接近 \(n\) 時,每層幾乎都不用做什么事。

建議看下面兩個並不復雜的證明,你能更加信任這個篩法,利於以后的擴展學習。

正確性(所有合數都會被標記)證明

設一合數 \(C\)(要篩掉)的最小質因數是 \(p_1\),令 \(B = C / p_1\)\(C = B × p_1\)),則 \(B\) 的最小質因數不小於 \(p_1\)(否則 \(C\) 也有這個更小因子)。那么當外層枚舉到 \(i = B\) 時,我們將會從小到大枚舉各個質數;因為 \(i = B\) 的最小質因數不小於 \(p_1\),所以 \(i\) 在質數枚舉至 \(p_1\) 之前一定不會break,這回\(C\) 一定會被 \(B × p_i\) 刪去。

核心:親愛的 \(B\) 的最小質因數必不小於 \(p_1\)

例:\(315 = 3 × 3 × 5 × 7\),其最小質因數是 \(3\)。考慮 \(i = 315 / 3 = 105\) 時,我們從小到大逐個枚舉質數,正是因為 \(i\) 的最小質因數不會小於 \(3\)(本例中就是 \(3\)),所以當枚舉 \(j = 1 (Prime[j] = 2)\) 時,\(i\) 不包含 \(2\) 這個因子,也就不會break,直到 \(Prime[j] = 3\) 之后才退出。

當然質數不能表示成“大於1的某數×質數”,所以整個流程中不會標記。

線性復雜度證明

注意這個算法一直使用“某數×質數”去篩合數,又已經證明一個合數一定會被它的最小質因數 \(p_1\) 篩掉,所以我們唯一要擔心的就是同一個合數是否會被“另外某數 × \(p_1\) 以外的質數”再篩一次導致浪費時間。設要篩的合數是 \(C\),設這么一個作孽的質數為 \(p_x\),再令 \(A = C / p_x\)\(A\) 中一定有 \(p_1\) 這個因子。當外層枚舉到 \(i = A\),它想要再篩一次 \(C\),卻在枚舉 \(Prime[j] = p_1\) 時,因為 \(i \mod Prime[j] == 0\) 就退出了。因而 \(C\) 除了 \(p_1\) 以外的質因數都不能篩它。

核心:罪惡的 \(A\) 中必有 \(p_1\) 這個因子。

例:\(315 = 3 × 3 × 5 × 7\)。首先,雖然看上去有兩個 \(3\),但我們篩數的唯一一句話就是

			isPrime[ i*Prime[j] ] = 0;

所以,\(315\) 只可能用 \(105 × 3\)\(63 × 5\)\(45 × 7\) 這三次篩而非四次。然后,非常抱歉,后兩個 \(i = 63, i = 45\) 都因為貪婪地要求對應的質數 \(Prime[j]\)\(5\)\(7\),而自己被迫擁有 \(3\) 這個因數,因此他們內部根本枚舉不到 \(5\)\(7\),而是枚舉到 \(3\) 就break了。

以上兩個一證,也就無可多說了。


更新日志:

2019-02-22 原理簡化;用詞修改或訂正。

2019-04-02 一些用詞更准確;加入更多括號內的注釋,減少回看上文的需要。


免責聲明!

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



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