Miller-Rabin và Pollard's Rho
Khi cần kiểm tra một số rất lớn (tới ) có phải số nguyên tố hay không, hoặc phân tích nó thành tích các thừa số nguyên tố, cách ngây thơ thử mọi ước tới tốn phép tính — quá chậm. Miller-Rabin kiểm tra nguyên tố trong (chỉ vài chục phép nhân lớn), còn Pollard's Rho tìm một ước không tầm thường của trong khoảng kỳ vọng. Kết hợp hai thuật toán này, ta phân tích thừa số một số gần như tức thì.
Ý tưởng / Trực giác
Miller-Rabin — tại sao đúng?
Xuất phát từ định lý Fermat nhỏ: nếu nguyên tố thì với mọi không chia hết cho . Nhưng điều ngược lại không đúng (có số giả nguyên tố Fermat, vd số Carmichael như qua được test Fermat với mọi cơ sở). Miller-Rabin mạnh hơn nhờ khai thác thêm một tính chất:
Trong trường khi nguyên tố, phương trình chỉ có đúng hai nghiệm là và . Viết với lẻ. Dãy
là dãy "bình phương liên tiếp", kết thúc ở (theo Fermat). Lần đầu tiên dãy chạm , phần tử ngay trước nó là một căn bậc hai của . Nếu nguyên tố, căn đó bắt buộc là hoặc . Vậy điều kiện cần để nguyên tố là:
- , hoặc
- tồn tại sao cho .
Nếu với cơ sở nào đó cả hai điều này đều sai thì ta tìm được một căn bậc hai "lạ" của — bằng chứng chắc chắn là hợp số. Khi đó gọi là nhân chứng (witness). Định lý cho biết với mỗi hợp số lẻ, ít nhất số cơ sở là nhân chứng, nên thử nhiều cơ sở thì xác suất sai giảm rất nhanh. Với , dùng cố định 12 cơ sở là xác định (deterministic) — không còn xác suất.
Pollard's Rho — tại sao tìm được ước?
Giả sử có ước nguyên tố . Sinh một dãy "giả ngẫu nhiên" bằng hàm :
Xét dãy này theo modulo . Vì chỉ có lớp dư, theo nguyên lý chuồng bồ câu dãy sẽ lặp lại sau khoảng bước (nghịch lý ngày sinh), tạo thành hình chữ "ρ" (rho). Khi nhưng (theo modulo ), thì là bội của nhưng không phải bội của , nên
là một ước thực sự của . Ta dùng phát hiện chu trình kiểu Floyd/Brent để bắt cặp đó. Vì nên chỉ cần bước kỳ vọng.
Pollard's Rho chỉ cho một ước bất kỳ (chưa chắc nguyên tố). Ghép với Miller-Rabin để biết khi nào dừng: nếu phần còn lại đã nguyên tố thì ghi nhận, ngược lại tiếp tục tách.
Ví dụ chạy tay
Miller-Rabin với (số Carmichael), cơ sở
, vậy , .
Tính . Vì và , ta bình phương liên tiếp và so với :
bước j giá trị x = 2^(2^j · 35) mod 561 so với 1? so với 560?
0 263 không không
1 263^2 mod 561 = 166 không không
2 166^2 mod 561 = 67 không không
3 67^2 mod 561 = 1 <-- căn bậc 2 của 1 mà KHÁC ±1!
Tại bước ta được trong khi phần tử trước () không phải . Đây là căn bậc hai "lạ" của 561 là hợp số (đúng: ). Test Fermat thường bị qua mặt, nhưng Miller-Rabin bắt được ngay.
Pollard's Rho với , ,
. Dùng Floyd: chạy 1 bước, chạy 2 bước, xét .
vòng x y |x - y| gcd(|x-y|, 8051)
1 5 26 21 1
2 26 7474 7448 1
3 677 871 194 97 <-- > 1, tìm thấy ước!
là ước của . Thật vậy . Chỉ sau 3 vòng thay vì thử chia tới lần.
Cài đặt
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
// Nhân modulo an toàn cho tới 2^63 nhờ __int128 (tránh tràn long long)
ll mulmod(ll a, ll b, ll m) {
return (__int128)a * b % m;
}
// Lũy thừa nhanh: a^b mod m
ll power(ll a, ll b, ll m) {
ll res = 1; a %= m;
for (; b > 0; b >>= 1) {
if (b & 1) res = mulmod(res, a, m);
a = mulmod(a, a, m);
}
return res;
}
// Một lần kiểm tra Miller-Rabin với cơ sở a. true = "có thể nguyên tố"
bool check_composite(ll n, ll a, ll d, int r) {
ll x = power(a, d, n);
if (x == 1 || x == n - 1) return false; // qua được, chưa kết luận hợp số
for (int i = 0; i < r - 1; i++) {
x = mulmod(x, x, n);
if (x == n - 1) return false; // gặp -1, qua được
}
return true; // không bao giờ gặp 1 hay -1 => hợp số
}
bool is_prime(ll n) {
if (n < 2) return false;
// Tách n - 1 = 2^r * d, d lẻ
int r = 0; ll d = n - 1;
while ((d & 1) == 0) { d >>= 1; r++; }
// 12 cơ sở này XÁC ĐỊNH với mọi n < 3.3 * 10^24
for (ll a : {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}) {
if (n == a) return true; // n nhỏ trùng cơ sở thì nguyên tố
if (check_composite(n, a, d, r)) return false;
}
return true;
}
// Pollard's Rho (bản Brent): trả về một ước KHÔNG TẦM THƯỜNG của n
ll pollard(ll n) {
if (n % 2 == 0) return 2;
ll x = rand() % (n - 2) + 2, y = x, c = rand() % (n - 1) + 1, d = 1;
auto f = [&](ll v) { return (mulmod(v, v, n) + c) % n; };
while (d == 1) {
x = f(x);
y = f(f(y)); // y chạy nhanh gấp đôi (Floyd)
d = __gcd(abs(x - y), n);
}
return d == n ? pollard(n) : d; // xui (d==n) thì đổi c, thử lại
}
// Phân tích đệ quy, gom vào map<thừa số, số mũ>
void factorize(ll n, map<ll,int>& factors) {
if (n == 1) return;
if (is_prime(n)) { factors[n]++; return; }
ll d = pollard(n);
factorize(d, factors);
factorize(n / d, factors);
}
int main() {
srand(time(0));
ll n; cin >> n;
map<ll,int> f;
factorize(n, f);
for (auto [p, e] : f) cout << p << "^" << e << " ";
}
Độ phức tạp
Miller-Rabin: mỗi cơ sở cần một lũy thừa modulo phép nhân, mỗi phép nhân __int128 là (hoặc nếu tính theo độ dài bit), thêm lần bình phương. Với cơ sở cố định (), tổng cộng — chỉ vài nghìn phép tính cho , so với của cách chia thử.
Pollard's Rho: với ước nguyên tố nhỏ nhất , kỳ vọng bước, mà nên bước; mỗi bước có một gcd , vậy mỗi lần tách. Phân tích đầy đủ gọi đệ quy tầng (mỗi tầng cắt đôi giá trị), tổng .
Bộ nhớ: cho ngăn xếp đệ quy và cho map — không đáng kể, thực tế.
⚠️ Lỗi thường gặp
- Tràn số khi nhân modulo. Với tới , phép
a * bcủa hai số vượt xa giới hạnlong long(). Phải ép kiểu(__int128)a * b % m. Quên ép kiểu là nguyên nhân sai phổ biến nhất, và lỗi chỉ lộ ra với test lớn. - Quên trường hợp nhỏ trùng cơ sở. Nếu thì trong vòng lặp Miller-Rabin có thể tính
power(a, d, n)vớia == n(tức ), cho kết quả và kết luận sai "hợp số". Phải kiểmif (n == a) return true;(hoặc xử lý và các số nhỏ trước). - Pollard's Rho bị kẹt khi là số nguyên tố. Nếu lỡ gọi
pollardtrên một số nguyên tố, vòngwhile (d == 1)có thể chạy rất lâu rồi mới trả . Luôn gọiis_primetrước để chặn, và chỉpollardtrên hợp số. - Lấy (gcd ra chính nó). Khi trước khi tách được, gcd trả về — không phải ước thực sự. Phải đổi hằng số (hoặc điểm xuất phát) và thử lại, như dòng
return d == n ? pollard(n) : d;. Bỏ qua bước này sẽ cho ước sai . absvớilong long. Dùngabs(x - y)mà include sai header có thể gọiabs(int)làm cụt giá trị. Dùngllabs,std::abs(với<cmath>/<cstdlib>đúng) hoặc tự viếtx > y ? x - y : y - x.- Dùng quá ít cơ sở rồi tưởng là xác định. Chỉ cơ sở không đủ — có hợp số qua mặt (vd ). Để xác định tới , dùng đủ bộ 12 cơ sở (hoặc bộ 7 cơ sở đã chứng minh cho : ).
Biến thể / Mở rộng
- Miller-Rabin xác suất: khi vượt (vd dùng số nguyên lớn), chọn ngẫu nhiên cơ sở; xác suất báo sai .
- Brent's improvement: gộp nhiều hiệu vào một tích rồi mới
gcdmột lần (giảm số lần gcd ~ lần) — phiên bản trong nội dung cũ của trang chính là biến thể này. - Ứng dụng phân tích thừa số: từ tính được hàm Euler , số ước , tổng ước, căn nguyên thủy... Xem thêm Toán học.
Bài tập luyện
- Kiểm Tra Số Nguyên Tố (primechk) — (Beginner) Kiểm tra một số có nguyên tố không — khởi động với chia thử, rồi thử cài Miller-Rabin để làm quen điều kiện Fermat mạnh.
- Số nguyên tố tiếp theo (nextprime) — (Intermediate) Tìm số nguyên tố nhỏ nhất lớn hơn ; chia thử đã chậm, Miller-Rabin là lời giải tự nhiên.
- Phân tích ước số (divanal) — (Intermediate) Tính số ước, tổng ước, tích ước từ phân tích thừa số nguyên tố — luyện các công thức ước số sau khi đã có phân tích.
- Bội số nguyên tố (primemult) — (Advanced) Đếm số trong với chia hết cho ít nhất một trong số nguyên tố — bao hàm–loại trừ trên tập thừa số, cần xử lý số rất lớn.