線性篩素數 - 尤拉篩 (包含正確性和複雜度的證明)
2018-11-06 更新
想要快速地篩出一定上限內的素數?
下面這種方法可以保證範圍內的每個合數都被刪掉(在 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\) 的最小質因數肯定是 \(Prime[j]\)(否則在列舉 \(Prime[\text{小於} j \text{的數}]\) 時就break)。嗯,這說明 \(j\) 符合條件。
** \(i × Prime[<j]\) 的最小質因數一定是那個 \(Prime[<j]\)(如果不是,則最小質因數更早被列舉到,就更早break)。這說明 \(j\) 之前都符合條件。**
** \(i × Prime[>j]\) 的最小質因數一定是 \(Prime[j]\)(緊接著證明)。這說明,如果 \(j\) 繼續遞增,是不符合條件的。**
具體說:
①:如果 \(i \mod Prime[j] == 0\),說明 \(i\) 有 \(Prime[j]\) 這個因子。那麼 \(i\) 可以表示為 \(Prime[j] × k\)。
②:如果我們再往下,下一個被篩掉的合數將是 \(i × Prime[j+1]\)(此時我們期望 \(Prime[j+1]\) 是這個合數的最小質因數,但是……),
③:根據①,該合數可以表示為 \((Prime[j] × k) × Prime[j+1]\)。
** ④:看, \(Prime[j]\) 明顯就是該合數的一個更小的質因數,說明該合數的最小質因數並不是 \(Prime[j+1]\),那麼用 \(Prime[j+1]\) 去篩這個合數是不符合要求的。所以不應該在這裡篩掉。**
⑤:後面 \(i * Prime[j+2]\) 等等與此相同,他們都一定有更小的質因數 \(Prime[j]\),就不要用 \(Prime[j+2]\) 這個更大的質因數去篩。
小提示:
當 \(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了。
以上兩個一證,也就無可多說了。