在我們需要判斷一個數是否是素數的時候,最容易想到的就是那個熟悉的O(√n)的演算法。那個演算法非常的簡單易懂,但如果我們仔細想想,當n這個數字很大的時候,這個演算法其實是不夠用的,時間複雜度會相對比較高。
怎麼解決呢?我們先來了解一下“費馬小定理”。假設我們有一個素數p,且另一個數a和p互素,就可以得到ap-1≡1(mod p)。這個定理很巧妙啊,有人就想了,能不能通過費馬小定理來判斷一個數是否是素數呢?也就是說,當我們判斷一個數p是否是素數時,只需要判斷ap-1≡1(mod p)是否成立即可。這裡的a因為是任意數,乾脆就讓它等於2,那麼判斷一個數p是否是素數就轉化成了判斷2p-1≡1(mod p)是否成立。乍一看,這好像沒什麼問題。當這個式子不成立時,p一定是一個合數,這沒毛病;但是當式子成立的時候p就一定是素數嗎?我們舉個反例。當p = 341時,2340≡1(mod 341)成立,然而很不巧,341 = 11 * 31是一個純正的合數。這就是數學中所說的,對於所有的a都存在對應的偽素數。(ps:這個問題並不能完全通過改變基數解決)
那我們該怎麼辦呢?其實很簡單,只需要進行“二次探測”把偽素數揪出來就ok了。這可不是亂改,是有依據的:當p為素數時,方程x2≡1(mod p)有兩個根 x = 1 和 x = p - 1。這兩個根被我們賦予了一個奇怪的名字:平凡平方根。那麼,判斷一個數p是否是素數這個問題就又被我們轉化,變成了判斷在模p意義下是否存在1的非平凡平方根,若存在則p為合數,反之則為素數。這一測試被我們“親切地”稱為“Miller - Rabin測試”。
具體操作步驟如下:
①選取多個基數a進行測試;
②尋找模p為1的非平凡平方根;令p - 1 = 2t*u(t >= 1, u為奇數),ap-1 = a2t*u = a2t ,先算出x=au (mod p),再把 x 平方 t 次,每次模上 p,這樣我們就得到了一個長度為 t + 1 的序列。我們希望這個序列以 1 結尾。若中間某一項為 1,則這一項的前一項必須為 1 或 n - 1,否則p就是合數。
在Miller - Rabin測試中進行s次測試,這並不代表這項測試是簡單地驗證費馬小定理,它大大降低了出錯的概率,研究表明,Miller - Rabin測試的出錯概率至多為 2-s 這可以說是非常小了。所以不用擔心它的準確度和嚴謹性。
程式碼如下:(你沒看錯就是rand())
#include<bits/stdc++.h> using namespace std; int pow(long long a, long long b, long long n) { long long ans = 1; while (b) { if (b & 1) { ans = ans * a % n; } a = a * a % n; b >>= 1; } return ans; } bool judge(long long n, long long a) { long long u = 0, t = n - 1; while (t % 2 == 0) {u++; t /= 2;} long long x = pow(a, t, n); for (int i = 1; i <= u; i++) { long long nx = x * x % n; if ((nx == 1) && (x != 1) && (x != n - 1)) return true; x = nx; } if (x != 1) return true; return false; } bool miller(long long n, int s) { if (n == 2) return true; if (n < 2 || n % 2 == 0) return false; for (int i = 1; i <= s; i++) { long long a = rand() % (n - 2) + 2; if (judge(n, a)) return false; } return true; } int main() { int t; cin >> t; long long a; while (t--) { cin >> a; if (miller(a, 10)) { cout << "yes" << endl; } else cout << "no" << endl; } return 0; }