有一個正整數 \(n\) ,試判斷 \(n\) 是不是質數。
經典模板了屬於是
主要有質數篩、枚舉因子、Miller Rabin 算法三種做法
1. 質數篩
分為埃氏篩和歐拉篩(線性篩)兩種
埃氏篩應該是判斷質數的最基礎方法了
從 \(2\) 開始從小到大依次枚舉整數
如果沒被篩過就說明是質數,同時應將其所有倍數(除本身外)篩去
v[1]=1; // 值為 1 表示非質數,0 表示是質數
for (int i=2; i<=n; ++i) if (!v[i])
for (int j=(i<<1); j<=n; j+=i) v[j]=1;
時間復雜度 \(O(n\log \log n)\)
歐拉篩同樣是篩倍數,但用了一些技巧將時間復雜度降至了 \(O(n)\)
v[1]=1;
for (int i=2; i<=n; ++i) {
if (!v[i]) p[++cnt]=i;
for (int j=1; j<=cnt&&p[j]*i<=n; ++j) {
v[p[j]*i]=1;
if (i%p[j]==0) break;
}
}
關鍵在於每個數恰好被其最小質因子篩一次
看懂這個結論之后 正確性和時間復雜度就都很顯然了
2. 枚舉因子
質數的定義:只有兩個正因數(\(1\) 和本身)的自然數是質數。
所以只需判斷 \(2\sim n-1\) 中有沒有 \(n\) 的因子
\(x\) 的因子是成對出現的,每一對因子中較小者必然不超過 \(\sqrt n\) ,因此從 \(2\) 枚舉到 \(\sqrt n\) 即可
int isp(int x) { //判斷質數,是則返回 1 ,否則返回 0
if (x==1) return 0; // 1 既不是質數也不是合數
for (int i=2; i*i<=x; ++i)
if (x%i==0) return 0;
return 1;
}
時間復雜度為 \(O(\sqrt n)\)
細想一下發現僅枚舉質因子即可保證正確性
所以有一種優化思路是盡量避免枚舉合數
考慮如下結論:
大於 \(3\) 的質數必然和 \(6\) 的倍數相鄰。
設 \(k\) 為正整數,則 \(6k-2,6k,6k+2,6k+3\) 顯然都為合數
因此只需枚舉形如 \(6k\pm 1(k\in\mathbb N^*)\) 的數即可
int isp(int x) {
if (x<5) return (x==2||x==3); // 注意 2,3 需要特判
if (x%6!=1&&x%6!=5) return 0;
for (int i=5; i*i<=x; i+=6)
if (x%i==0||x%(i+2)==0) return 0;
return 1;
}
時間復雜度依然為 \(O(\sqrt n)\) ,但實際運行要比優化前快幾倍,而且代碼也不長,日常做題推薦使用
3. Miller-Rabin 素性檢測
這篇文章講得非常詳細了:link
時間復雜度 \(O(k\log n)\) ,其中 \(k\) 為測試次數
OI 中可以選擇固定的幾個常數做測試,在保證准確的前提下:
\(2^{32}\) 以內選擇 \(2,7,61\) 三個數即可
\(10^{16}\) 以內選擇 \(2,3,7,13,61,24251\) 六個數即可
數據范圍更大時可以換成隨機數,具體實現可參考上面提到的文章
4. 總結
| 算法 |
時間復雜度
|
空間復雜度 |
|---|---|---|
| 線性篩 | \(O(n)-O(1)\) | \(O(n)\) |
| 埃氏篩 | \(O(n\log \log n)-O(1)\) | \(O(n)\) |
| 暴力 | \(O(\sqrt n)\) | \(O(1)\) |
| Miller-Rabin | \(O(k\log n)\) | \(O(k)\) |
\(O(n)-O(1)\) 表示預處理 \(O(n)\) ,回答單次詢問 \(O(1)\) (埃氏篩時間復雜度同理)
如果數據范圍較小,用暴力隨便搞搞就行了
如果詢問次數很多且 \(n\) 不是很大,應使用篩法(推薦線性篩)
Miller-Rabin 代碼量大,寫起來比較耗時,盡量少用
考場上很少會出現必須用 Miller-Rabin 的情況,所以這個算法不用練得太熟 差不多得了
