Wiki Thuật toán Toán học Miller-Rabin và Pollard's Rho

Miller-Rabin và Pollard's Rho

huunguyen huunguyen Updated Tháng sáu 1, 2026

Khi cần kiểm tra một số N rất lớn (tới 1018) 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 N tốn O(N)109 phép tính — quá chậm. Miller-Rabin kiểm tra nguyên tố trong O(Klog2N) (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 N trong khoảng O(N1/4) 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ố 1018 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 N nguyên tố thì aN11(modN) với mọi a không chia hết cho N. Nhưng điều ngược lại không đúng (có số giả nguyên tố Fermat, vd số Carmichael như 561 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 N khi N nguyên tố, phương trình x21 chỉ có đúng hai nghiệmx1x1. Viết N1=2r·d với d lẻ. Dãy

ad, a2d, a4d, , a2r1d, a2rd=aN1

là dãy "bình phương liên tiếp", kết thúc ở 1 (theo Fermat). Lần đầu tiên dãy chạm 1, phần tử ngay trước nó là một căn bậc hai của 1. Nếu N nguyên tố, căn đó bắt buộc1 hoặc 1. Vậy điều kiện cần để N nguyên tố là:

  • ad1(modN), hoặc
  • tồn tại j[0,r) sao cho a2jd1(modN).

Nếu với cơ sở a 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 1 — bằng chứng chắc chắn Nhợp số. Khi đó a gọi là nhân chứng (witness). Định lý cho biết với mỗi hợp số lẻ, ít nhất 3/4 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 N<3.3×1024, dùng cố định 12 cơ sở {2,3,5,,37}xác định (deterministic) — không còn xác suất.

Pollard's Rho — tại sao tìm được ước?

Giả sử N có ước nguyên tố pN. Sinh một dãy "giả ngẫu nhiên" bằng hàm f(x)=(x2+c)modN:

x0, x1=f(x0), x2=f(x1), 

Xét dãy này theo modulo p. Vì chỉ có p lớp dư, theo nguyên lý chuồng bồ câu dãy ximodp sẽ lặp lại sau khoảng O(p) bước (nghịch lý ngày sinh), tạo thành hình chữ "ρ" (rho). Khi xixj(modp) nhưng xixj (theo modulo N), thì |xixj|bội của p nhưng không phải bội của N, nên

gcd(|xixj|, N)

là một ước thực sự của N. Ta dùng phát hiện chu trình kiểu Floyd/Brent để bắt cặp (i,j) đó. Vì pN nên chỉ cần O(p)=O(N1/4) 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 N=561 (số Carmichael), cơ sở a=2

N1=560=24·35, vậy d=35, r=4.

Tính x=235mod561=263. Vì x1x560, ta bình phương liên tiếp và so với N1=560:

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 j=3 ta được x=1 trong khi phần tử trước (67) không phải ±1. Đây là căn bậc hai "lạ" của 1 561 là hợp số (đúng: 561=3·11·17). Test Fermat thường bị 561 qua mặt, nhưng Miller-Rabin bắt được ngay.

Pollard's Rho với N=8051, c=1, x0=2

f(x)=(x2+1)mod8051. Dùng Floyd: x chạy 1 bước, y chạy 2 bước, xét gcd(|xy|,N).

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!

gcd=97 là ước của 8051. Thật vậy 8051=97×83. Chỉ sau 3 vòng thay vì thử chia tới 805189 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 O(logN) phép nhân, mỗi phép nhân __int128O(1) (hoặc O(logN) nếu tính theo độ dài bit), thêm rlogN lần bình phương. Với K cơ sở cố định (K=12), tổng cộng O(Klog2N) — chỉ vài nghìn phép tính cho N1018, so với O(N)109 của cách chia thử.

Pollard's Rho: với ước nguyên tố nhỏ nhất p, kỳ vọng O(p) bước, mà pN nên O(N1/4) bước; mỗi bước có một gcd O(logN), vậy O(N1/4logN) mỗi lần tách. Phân tích đầy đủ gọi đệ quy O(logN) tầng (mỗi tầng cắt đôi giá trị), tổng O(N1/4log2N).

Bộ nhớ: O(logN) cho ngăn xếp đệ quy và O(#thừa số phân biệt) cho map — không đáng kể, O(1) thực tế.

⚠️ Lỗi thường gặp

  • Tràn số khi nhân modulo. Với N tới 1018, phép a * b của hai số <N vượt xa giới hạn long long (9.2×1018). 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 n nhỏ trùng cơ sở. Nếu n=2,3,,37 thì trong vòng lặp Miller-Rabin có thể tính power(a, d, n) với a == n (tức a0), cho kết quả 0 và kết luận sai "hợp số". Phải kiểm if (n == a) return true; (hoặc xử lý n<2 và các số nhỏ trước).
  • Pollard's Rho bị kẹt khi n là số nguyên tố. Nếu lỡ gọi pollard trên một số nguyên tố, vòng while (d == 1) có thể chạy rất lâu rồi mới trả d=n. Luôn gọi is_prime trước để chặn, và chỉ pollard trên hợp số.
  • Lấy d=n (gcd ra chính nó). Khi xy(modN) trước khi tách được, gcd trả về n — không phải ước thực sự. Phải đổi hằng số c (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 =n.
  • abs với long long. Dùng abs(x - y) mà include sai header có thể gọi abs(int) làm cụt giá trị. Dùng llabs, std::abs (với <cmath>/<cstdlib> đúng) hoặc tự viết x > y ? x - y : y - x.
  • Dùng quá ít cơ sở rồi tưởng là xác định. Chỉ cơ sở {2,3} không đủ — có hợp số qua mặt (vd 1373653). Để xác định tới 1018, dùng đủ bộ 12 cơ sở (hoặc bộ 7 cơ sở đã chứng minh cho <3.2×1018: {2,3,5,7,11,13,17}).

Biến thể / Mở rộng

  • Miller-Rabin xác suất: khi N vượt 1024 (vd dùng số nguyên lớn), chọn ngẫu nhiên k cơ sở; xác suất báo sai 4k.
  • Brent's improvement: gộp nhiều hiệu |xy| vào một tích rồi mới gcd một lần (giảm số lần gcd ~logN 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ừ N=piei tính được hàm Euler φ(N)=N(11/pi), số ước (ei+1), 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ố N106 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 n1012; chia thử O(n) đã 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 [1,n] với n1018 chia hết cho ít nhất một trong k 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.
gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.46 awk 1.3.4 gcc 16.1.0 csc 6.12.0.200 g++ 16.1.0 g++-themis 16.1.0 g++17 16.1.0 g++20 16.1.0 g++23 16.1.0 clang++ 22.1.6 dmd 2.112.0 dart 3.12.1 gforth 0.7.3 gfortran 12.2.0 go 1.26.3 groovyc 5.0.6 javac 25.0.3 node 26.2.0 kotlinc 2.3.21 sbcl 2.2.9 lua 5.4.8 nim 2.2.10 fpc 3.2.2 fpc-themis 3.2.2 perl 5.36.0 php 8.5.6 pike 8.0 pypy3 7.3.23 python3 3.14.5 racket 8.7 ruby 4.0.5 rustc 1.96.0 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 tclsh 8.6 bun 1.3.14 deno 2.8.1 v 0.5.1 zig 0.16.0