Number Theory
🔢 Kiểm tra số nguyên tố: Trial, Sieve, Segmented Sieve & Miller–Rabin
Bài học nâng cấp các phương pháp kiểm tra số nguyên tố theo hướng dùng trong thi lập trình hiện nay: chọn đúng thuật toán theo kích thước dữ liệu, sửa các độ phức tạp chưa chuẩn, bổ sung Miller–Rabin xác định cho 64-bit và cách dùng an toàn trong C++/Python.
Trial Division1 số nhỏ
SieveNhiều truy vấn
SegmentedĐoạn [L,R] rất lớn
Miller–RabinN đến 2⁶⁴ và hơn
Mục tiêu và bức tranh tổng quan
Học xong cần làm được
- Hiểu số nguyên tố và vì sao chỉ cần thử đến
√n. - Biết chọn giữa chia thử, sàng thường, sàng đoạn và Miller–Rabin.
- Cài đặt code C++/Python an toàn cho dữ liệu lớn.
- Nhận ra lỗi tràn số, sai độ phức tạp, dùng thuật toán không phù hợp.
Cập nhật quan trọng
- Sàng Eratosthenes chuẩn có độ phức tạp O(n log log n).
- Với 64-bit, Miller–Rabin có thể kiểm tra chính xác bằng bộ base cố định.
- C++ hiện đại dùng
__int128để nhân modulo an toàn. - Python dùng
pow(a, d, n)đã tối ưu và an toàn cho số lớn.
| Dạng bài | Thuật toán nên dùng | Độ phức tạp | Ghi chú |
|---|---|---|---|
| Kiểm tra 1 số nhỏ, ít test | Trial Division | O(√n) | Đủ tốt với n ≤ 10⁹ và ít test. |
| Nhiều truy vấn trong [1,N] | Sàng Eratosthenes | O(n log log n), bộ nhớ O(n) | Phù hợp N khoảng 10⁷, tùy bộ nhớ. |
| Tìm prime trong [L,R], R lớn | Segmented Sieve | O(√R log log √R + độ dài đoạn) | Dùng khi R lớn nhưng R-L vừa phải. |
| Kiểm tra n ≤ 2⁶⁴ | Miller–Rabin deterministic bases | O(k log n) | Rất nhanh. |
| Phân tích thừa số 64-bit | Miller–Rabin + Pollard Rho | Nâng cao | Miller–Rabin chỉ kiểm tra prime/composite. |
1. Trial Division – chia thử đến √n
Chia thử là nền tảng: nếu n là hợp số thì nó có ít nhất một ước không vượt quá √n.
Nhấn “Bước tiếp” để thử các ước từ 2 đến √n.
Phân tích thuật toán
- Ý tưởng: nếu
ncó ước khác 1 và chính nó, thì tồn tại một ước không vượt quá√n. - Tối ưu cơ bản: xử lý riêng số chẵn, sau đó chỉ thử các ước lẻ
3, 5, 7, .... - Độ phức tạp:
O(√n), bộ nhớO(1). - Khi dùng: phù hợp khi chỉ kiểm tra ít số, hoặc
nkhông quá lớn.
bool isPrime(long long n) {
if (n < 2) return false;
if (n % 2 == 0) return n == 2;
for (long long i = 3; i <= n / i; i += 2)
if (n % i == 0) return false;
return true;
}def is_prime(n):
if n < 2:
return False
if n % 2 == 0:
return n == 2
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2
return TrueGiải thích code
if (n < 2) return false;: loại 0, 1 và số âm vì không phải số nguyên tố.if (n % 2 == 0) return n == 2;: chỉ số 2 là nguyên tố chẵn; mọi số chẵn lớn hơn 2 đều là hợp số.i <= n / i: tương đươngi*i <= nnhưng tránh tràn số trong C++ khinlớn.i += 2: bỏ qua toàn bộ số chẵn, giảm khoảng một nửa số phép thử.
Lưu ý C++: dùng
i <= n / i thay vì i * i <= n để tránh tràn số.2. Sàng Eratosthenes
Nếu p là nguyên tố, ta gạch các bội số của p bắt đầu từ p².
Nhấn “Bước tiếp” để gạch bội số.
Phân tích thuật toán
- Ý tưởng: ban đầu coi mọi số từ 2 đến N là nguyên tố, sau đó gạch các bội của từng số nguyên tố.
- Vì sao bắt đầu từ
p*p? Các bội nhỏ hơnp*pđã bị gạch bởi các ước nguyên tố nhỏ hơnp. - Độ phức tạp chuẩn:
O(n log log n), bộ nhớO(n). - Khi dùng: tốt cho nhiều truy vấn kiểm tra nguyên tố trong phạm vi vừa, ví dụ
N ≤ 10^7.
vector<bool> sieve(int n) {
vector<bool> isPrime(n + 1, true);
if (n >= 0) isPrime[0] = false;
if (n >= 1) isPrime[1] = false;
for (long long p = 2; p * p <= n; p++)
if (isPrime[p])
for (long long x = p * p; x <= n; x += p)
isPrime[x] = false;
return isPrime;
}def sieve(n):
is_prime = [True] * (n + 1)
if n >= 0: is_prime[0] = False
if n >= 1: is_prime[1] = False
p = 2
while p * p <= n:
if is_prime[p]:
for x in range(p * p, n + 1, p):
is_prime[x] = False
p += 1
return is_primeGiải thích code
isPrime[0] = isPrime[1] = false: 0 và 1 không phải số nguyên tố.- Vòng ngoài
p * p <= n: chỉ cần xét prime đến√n. - Vòng trong
x = p*p; x += p: gạch lần lượt các bội số củap. - Sau khi sàng xong,
isPrime[x] == truenghĩa làxlà số nguyên tố.
Kiến thức chuẩn: Sàng Eratosthenes có thời gian
O(n log log n) và bộ nhớ O(n).3. Segmented Sieve – sàng trên đoạn [L, R]
Khi R rất lớn, không thể tạo mảng từ 1 đến R. Ta chỉ tạo mảng cho đoạn [L,R] và dùng prime nhỏ đến √R để gạch.
Sàng prime ≤ √R
→
Tạo mark[R-L+1]
→
Gạch bội trong đoạn
→
Ô còn lại là prime
Nhập đoạn có độ dài nhỏ để quan sát.
Phân tích thuật toán
- Vấn đề: nếu
Rrất lớn, không thể tạo mảng từ 1 đếnR. - Ý tưởng: chỉ tạo mảng cho đoạn cần xét
[L,R], rồi dùng các prime nhỏ≤ √Rđể gạch hợp số trong đoạn. - Điểm bắt đầu gạch:
max(p*p, ceil(L/p)*p). - Bộ nhớ:
O(√R + R-L+1), phù hợp khiRlớn nhưng độ dài đoạn vừa phải.
vector<long long> segmentedSieve(long long L, long long R) {
long long limit = sqrtl(R) + 1;
vector<bool> small(limit + 1, true);
small[0] = small[1] = false;
for (long long p = 2; p * p <= limit; p++)
if (small[p])
for (long long x = p * p; x <= limit; x += p)
small[x] = false;
vector<long long> primes;
for (long long p = 2; p <= limit; p++)
if (small[p]) primes.push_back(p);
vector<bool> mark(R - L + 1, true);
for (long long p : primes) {
long long start = max(p * p, ((L + p - 1) / p) * p);
for (long long x = start; x <= R; x += p)
mark[x - L] = false;
}
vector<long long> ans;
for (long long i = 0; i <= R - L; i++)
if (L + i >= 2 && mark[i]) ans.push_back(L + i);
return ans;
}import math
def segmented_sieve(L, R):
limit = math.isqrt(R) + 1
small = [True] * (limit + 1)
if limit >= 0: small[0] = False
if limit >= 1: small[1] = False
p = 2
while p * p <= limit:
if small[p]:
for x in range(p * p, limit + 1, p):
small[x] = False
p += 1
primes = [p for p in range(2, limit + 1) if small[p]]
mark = [True] * (R - L + 1)
for p in primes:
start = max(p * p, ((L + p - 1) // p) * p)
for x in range(start, R + 1, p):
mark[x - L] = False
return [L + i for i in range(R - L + 1) if L + i >= 2 and mark[i]]Giải thích code
limit = sqrt(R) + 1: chỉ cần các prime nhỏ đến căn củaR.mark[x - L]: ánh xạ số thậtxtrong đoạn về chỉ số mảng bắt đầu từ 0.start = max(p*p, ((L+p-1)/p)*p): tìm bội đầu tiên củapnằm trong đoạn nhưng không gạch chínhp.- Cuối cùng, số
L+ilà prime nếumark[i]còn true vàL+i ≥ 2.
4. Miller–Rabin – kiểm tra nguyên tố hiện đại
Miller–Rabin rất nhanh cho số lớn. Với số 64-bit, dùng bộ cơ số cố định có thể cho kết quả xác định.
Bộ base an toàn hay dùng cho 64-bit:
{2, 325, 9375, 28178, 450775, 9780504, 1795265022}. Với Python, pow(a, d, n) đã là lũy thừa modulo nhanh.Nhập N và cơ số a để quan sát chuỗi bình phương.
Code mẫu Miller–Rabin 64-bit
Phân tích thuật toán
- Bước 1: tách
n-1 = 2^s · d, vớidlà số lẻ. - Bước 2: với mỗi cơ số
a, tínhx = a^d mod nbằng lũy thừa nhị phân. - Bước 3: bình phương liên tiếp
x; nếu gặpn-1thì cơ số này không chứng minh được n hợp số. - Độ phức tạp: khoảng
O(k log n)phép nhân modulo, vớiklà số cơ số kiểm tra. - Cập nhật: với số 64-bit, dùng bộ base cố định để kiểm tra xác định; C++ dùng
__uint128_tđể nhân modulo an toàn.
using u64 = unsigned long long;
using u128 = __uint128_t;
u64 mod_mul(u64 a, u64 b, u64 mod) {
return (u128)a * b % mod;
}
u64 mod_pow(u64 a, u64 d, u64 mod) {
u64 res = 1;
while (d > 0) {
if (d & 1) res = mod_mul(res, a, mod);
a = mod_mul(a, a, mod);
d >>= 1;
}
return res;
}
bool check_composite(u64 n, u64 a, u64 d, int s) {
u64 x = mod_pow(a, d, n);
if (x == 1 || x == n - 1) return false;
for (int r = 1; r < s; r++) {
x = mod_mul(x, x, n);
if (x == n - 1) return false;
}
return true;
}
bool isPrime64(u64 n) {
if (n < 2) return false;
for (u64 p : {2ULL,3ULL,5ULL,7ULL,11ULL,13ULL,17ULL,19ULL,23ULL,29ULL,31ULL,37ULL})
if (n % p == 0) return n == p;
int s = 0;
u64 d = n - 1;
while ((d & 1) == 0) d >>= 1, s++;
for (u64 a : {2ULL,325ULL,9375ULL,28178ULL,450775ULL,9780504ULL,1795265022ULL}) {
if (a % n == 0) continue;
if (check_composite(n, a, d, s)) return false;
}
return true;
}def is_prime_miller_rabin(n):
if n < 2:
return False
for p in [2,3,5,7,11,13,17,19,23,29,31,37]:
if n % p == 0:
return n == p
d = n - 1
s = 0
while d % 2 == 0:
d //= 2
s += 1
bases = [2, 325, 9375, 28178, 450775, 9780504, 1795265022]
for a in bases:
if a % n == 0:
continue
x = pow(a, d, n)
if x == 1 or x == n - 1:
continue
for _ in range(s - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False
return TrueGiải thích code
mod_mul: tínha*b mod nbằng__uint128_t, tránh tràn số khia,bgần giới hạn 64-bit.mod_pow: lũy thừa nhị phân modulo, dùng để tínha^d mod nnhanh trongO(log d).check_composite: kiểm tra một cơ sốacó chứng minh đượcnlà hợp số hay không.small_primes: lọc nhanh các số nhỏ và số chia hết cho prime nhỏ.bases: bộ cơ số đặc biệt giúp Miller–Rabin chính xác cho phạm vi 64-bit.
Với số vượt 64-bit trong C++: cần big integer library. Với Python, số nguyên lớn được hỗ trợ sẵn, nhưng Miller–Rabin với vài base là kiểm tra xác suất nếu vượt miền chứng minh của bộ base.
5. Cách chọn thuật toán trong đề thi
| Ràng buộc đề | Dấu hiệu | Chọn | Không nên chọn |
|---|---|---|---|
n ≤ 10⁹, ít test | Chỉ hỏi một vài số | Trial Division tối ưu | Sàng lớn |
N ≤ 10⁷, nhiều truy vấn | Cần kiểm tra prime nhiều lần | Sàng + prefix count | Chia thử từng truy vấn |
L,R ≤ 10¹², R-L ≤ 10⁶ | Đoạn lớn nhưng ngắn | Segmented Sieve | Sàng từ 1 đến R |
n ≤ 10¹⁸ | Kiểm tra từng số rất lớn | Miller–Rabin 64-bit | Trial Division |
| Cần phân tích thừa số | Không chỉ hỏi prime/composite | Miller–Rabin + Pollard Rho | Chỉ Miller–Rabin |
6. Lỗi học sinh hay nhầm
Nhầm độ phức tạp sàng.
Sàng Eratosthenes chuẩn là
Sàng Eratosthenes chuẩn là
O(n log log n).Quên xử lý 0, 1.
0 và 1 không phải số nguyên tố.Tràn số khi kiểm tra
C++ nên dùng
i*i ≤ n.C++ nên dùng
i ≤ n / i.Segmented Sieve gạch sai điểm bắt đầu.
Dùng
Dùng
max(p*p, ceil(L/p)*p).Miller–Rabin nhưng nhân modulo bị tràn.
C++ 64-bit nên dùng
C++ 64-bit nên dùng
__int128.7. Bài tập vận dụng
📘 Cơ bản
- Kiểm tra một số có phải nguyên tố không.
- Đếm số nguyên tố trong mảng nhỏ.
- Liệt kê số nguyên tố ≤ N.
📗 Trung bình
- Q truy vấn “x có phải prime không?”.
- Prefix count số lượng prime trong đoạn.
- Sàng SPF để phân tích thừa số nhiều số.
📙 Nâng cao
- Segmented Sieve cho [L,R] lớn.
- Tìm prime gần nhất ≥ N với N ≤ 10¹⁸.
- Đếm số gần nguyên tố trong đoạn.
🐉 HSG định hướng
- Miller–Rabin + Pollard Rho.
- Prime path bằng BFS.
- Prime + DP/Graph.
Quiz kiểm tra nhanh
Câu 1. Sàng Eratosthenes chuẩn có độ phức tạp thời gian nào?
Câu 2. Với n ≤ 10¹⁸, kiểm tra từng số rất lớn nên dùng gì?
Câu 3. Trong Segmented Sieve, vì sao dùng
max(p*p, ceil(L/p)*p)?Tổng kết
Không có một cách kiểm tra số nguyên tố tốt nhất cho mọi bài. Chia thử dùng cho ít số nhỏ, sàng dùng cho nhiều truy vấn trong phạm vi vừa, sàng đoạn dùng cho đoạn lớn, và Miller–Rabin là lựa chọn hiện đại cho số rất lớn.
💳 Quét mã ủng hộ tuỳ tâm nhé!