Nhập môn

Sàng nguyên tố Eratosthenes và kiểm tra số nguyên tố: code C++, Python

Để kiểm tra một số đơn lẻ n có phải số nguyên tố hay không, ta thử chia cho các ước từ 2 đến √n trong thời gian O(√n). Nhưng khi cần tìm tất cả các số nguyên tố từ 1 đến n hoặc trả lời hàng trăm nghìn truy vấn, thuật toán sàng nguyên tố Eratosthenes sẽ nhanh hơn gấp nhiều lần: thay vì thử chia, nó lần lượt gạch bỏ các bội số của từng số nguyên tố chỉ trong O(n log log n).

Ý chính
  • Kiểm tra 1 số đơn lẻ n: duyệt ước từ 2 đến i * i <= n mất O(√n).
  • Tìm mọi số nguyên tố từ 1 đến n: dùng Sàng Eratosthenes mất O(n log log n) — gần như tuyến tính O(n).
  • Trong sàng Eratosthenes, khi xét số nguyên tố p, ta luôn bắt đầu gạch bội từ p * p (vì mọi bội nhỏ hơn dạng k * p với k < p đã bị gạch từ lúc xét ước nguyên tố của k).

Phần 1: Kiểm tra một số nguyên tố đơn lẻ trong O(√n)

Số nguyên tố (prime number) là số nguyên dương lớn hơn 1 và chỉ có đúng hai ước nguyên dương là 1 và chính nó (ví dụ: 2, 3, 5, 7, 11, 13...). Số 0 và 1 không phải là số nguyên tố; số lớn hơn 1 mà không nguyên tố được gọi là hợp số (composite number).

Nếu một số n là hợp số, nó có thể viết dưới dạng tích của hai ước n = a × b với 1 < a ≤ b < n. Khi đó chắc chắn ước nhỏ hơn a phải thỏa mãn a × a ≤ a × b = n, tức a ≤ √n.

Vì vậy, để kiểm tra n có phải số nguyên tố không, bạn không bao giờ cần thử chia tới n - 1, mà chỉ cần thử các số i từ 2 tới khi i * i <= n:

  • Nếu gặp bất kỳ số i nào mà n % i == 0 → kết luận ngay n là hợp số (false).
  • Nếu đi hết vòng lặp mà không chia hết cho số nào → n chắc chắn là số nguyên tố (true).

Với n = 10¹², vòng lặp i * i <= n chỉ chạy tối đa 10⁶ bước (mất vài mili-giây). Tuy nhiên, nếu đề bài hỏi: "Đếm xem từ 1 đến 1.000.000 có bao nhiêu số nguyên tố" hoặc cho q = 100.000 truy vấn kiểm tra nguyên tố, việc gọi hàm O(√n) lặp đi lặp lại sẽ mất khoảng n√n bước và bắt đầu chậm. Đó là lúc ta cần Sàng nguyên tố Eratosthenes.

Phần 2: Ý tưởng của Sàng nguyên tố Eratosthenes

Thay vì cầm từng số x đi thử chia xem nó có ước không (phép chia lấy dư % rất tốn thời gian), nhà toán học cổ đại Eratosthenes làm ngược lại: cầm từng số nguyên tố p rồi đi gạch bỏ tất cả các bội số của p (2p, 3p, 4p...) bằng phép cộng nhảy bước!

Quy trình sàng trên đoạn từ 0 đến n diễn ra như sau:

  1. Tạo một mảng đánh dấu isPrime[0..n] ban đầu gán tất cả bằng true. Đánh dấu isPrime[0] = isPrime[1] = false.
  2. Duyệt p từ 2 đến khi p * p <= n:
    • Nếu isPrime[p] vẫn đang là true (nghĩa là p không phải bội của bất kỳ số nào nhỏ hơn nó → p là số nguyên tố):
    • Ta gạch bỏ các bội của p bắt đầu từ p * p trở đi: p * p, p * p + p, p * p + 2p, ... bằng cách gán isPrime[multiple] = false.
  3. Sau khi vòng lặp kết thúc, mọi vị trí i còn giữ isPrime[i] == true chính là toàn bộ các số nguyên tố từ 2 đến n.

Ví dụ từng bước: Sàng các số nguyên tố từ 1 đến 30

Hãy quan sát bảng sàng với n = 30. Vì 5² = 25 ≤ 30 còn 7² = 49 > 30, ta chỉ cần thực hiện gạch bội cho đúng 3 số nguyên tố là p = 2, p = 3 và p = 5:

1 bỏ 2 nguyên tố 3 nguyên tố 4 bội 2 5 nguyên tố 6 bội 2 7 nguyên tố 8 bội 2 9 bội 3 10 bội 2 11 nguyên tố 12 bội 2 13 nguyên tố 14 bội 2 15 bội 3 16 bội 2 17 nguyên tố 18 bội 2 19 nguyên tố 20 bội 2 21 bội 3 22 bội 2 23 nguyên tố 24 bội 2 25 bội 5 26 bội 2 27 bội 3 28 bội 2 29 nguyên tố 30 bội 2 Chỉ cần sàng p = 2 (từ 4), p = 3 (từ 9), p = 5 (từ 25) vì 7² = 49 > 30. Còn lại 10 số nguyên tố màu xanh!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Xanh: Nguyên tố | Đỏ: bội 2 | Cam: bội 3 | Lam: bội 5 (25)
Hình 1. Sàng Eratosthenes từ 1 đến 30: sau 3 lượt gạch bội của 2, 3 và 5, 10 ô màu xanh còn lại là tất cả các số nguyên tố.
Lượt xét p Điểm bắt đầu gạch p² Các hợp số mới bị gạch ở lượt này Vì sao không cần gạch các bội nhỏ hơn p²?
p = 2 2² = 4 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30 —
p = 3 3² = 9 9, 15, 21, 27 (12, 18, 24, 30 đã gạch từ lượt 2) Bội 2 × 3 = 6 đã bị gạch ở lượt p = 2
p = 4 Bỏ qua vì isPrime[4] == false — Mọi bội của 4 đều là bội của 2
p = 5 5² = 25 25 (30 đã gạch từ lượt 2) 2×5=10, 3×5=15, 4×5=20 đã bị gạch ở lượt 2 và 3

Code kiểm tra nguyên tố và Sàng Eratosthenes bằng C++ và Python

#include <iostream>
#include <vector>
using namespace std;

// 1. Kiểm tra một số đơn lẻ n có phải số nguyên tố không trong O(sqrt(n))
bool laSoNguyenTo(long long n) {
    if (n < 2) return false;
    for (long long i = 2; i * i <= n; ++i) {
        if (n % i == 0) return false;
    }
    return true;
}

// 2. Sàng nguyên tố Eratosthenes từ 0 đến n trong O(n log log n)
vector<bool> sangEratosthenes(int n) {
    vector<bool> isPrime(n + 1, true);
    isPrime[0] = isPrime[1] = false;
    for (int p = 2; p * p <= n; ++p) {
        if (isPrime[p]) {
            // Bắt đầu gạch từ p * p vì các bội nhỏ hơn đã bị gạch bởi ước nhỏ hơn p
            for (int multiple = p * p; multiple <= n; multiple += p) {
                isPrime[multiple] = false;
            }
        }
    }
    return isPrime;
}

int main() {
    vector<bool> p30 = sangEratosthenes(30);
    for (int i = 2; i <= 30; ++i) {
        if (p30[i]) cout << i << " ";  // 2 3 5 7 11 13 17 19 23 29
    }
    cout << "\n";

    vector<bool> p1m = sangEratosthenes(1000000);
    int dem = 0;
    for (int i = 2; i <= 1000000; ++i) {
        if (p1m[i]) dem++;
    }
    cout << dem << " " << laSoNguyenTo(999999937LL) << "\n";  // 78498 1
    return 0;
}

Độ phức tạp của Sàng Eratosthenes là bao nhiêu?

Với mỗi số nguyên tố p, vòng lặp trong chạy n / p bước. Tổng số bước của toàn bộ thuật toán là: n / 2 + n / 3 + n / 5 + n / 7 + n / 11 + ...

Theo định lý Mertens trong lý thuyết số, tổng nghịch đảo các số nguyên tố nhỏ hơn hoặc bằng n là 1/2 + 1/3 + 1/5 + ... ≈ ln(ln n). Do đó:

  • Thời gian: O(n log log n). Với n = 10⁷, log₂(log₂(10⁷)) ≈ 4.5, nghĩa là trung bình mỗi số từ 1 đến n chỉ tốn khoảng 4 phép gán — nhanh gần như một vòng lặp đơn O(n).
  • Bộ nhớ: O(n) cho mảng đánh dấu isPrime.
Học cùng giảng viên

Chủ đề này nằm trong Lecture 06: Number Theory I (Lý thuyết số I) của Khóa Orange. Xem đề cương

Mở rộng cực mạnh: Phân tích thừa số nguyên tố trong O(log x) bằng mảng SPF

Một ứng dụng rất hay gặp trong đề thi là cần phân tích hàng trăm nghìn số x (x ≤ 10⁶) ra thừa số nguyên tố. Nếu mỗi số bạn lại chạy vòng lặp O(√x) thì với 10⁵ truy vấn sẽ mất khoảng 10⁸ phép chia.

Thay vì chỉ lưu true / false trong bảng sàng, ta tạo mảng spf[i] (Smallest Prime Factor) lưu ước nguyên tố nhỏ nhất của số i:

  • Ban đầu gán spf[i] = i.
  • Trong lúc sàng, khi số nguyên tố p đi qua bội m, nếu spf[m] vẫn đang bằng m thì ta gán spf[m] = p.
  • Khi cần phân tích một số x bất kỳ, ta chỉ việc lấy spf[x] làm thừa số nguyên tố đầu tiên, rồi chia x = x / spf[x] lặp lại cho tới khi x == 1. Vì mỗi lần chia x giảm đi ít nhất một nửa, quá trình phân tích chỉ tốn tối đa log2 x phép chia!

Hãy xem ví dụ từng bước phân tích số x = 84 chỉ bằng cách tra bảng spf (không cần thử chia cho bất kỳ số nào):

Bước Giá trị hiện tại của x Tra bảng p = spf[x] Ghi nhận thừa số nguyên tố Phép chia tiếp theo x = x / p
1 84 spf[84] = 2 2 84 / 2 = 42
2 42 spf[42] = 2 2 42 / 2 = 21
3 21 spf[21] = 3 3 21 / 3 = 7
4 7 spf[7] = 7 7 7 / 7 = 1 (Dừng vì x == 1)

Kết quả phân tích 84 = 2² × 3¹ × 7¹ tìm được sau đúng 4 bước tra mảng! Từ danh sách các thừa số nguyên tố này (p₁^k₁ × p₂^k₂ × ...), bạn còn tính được ngay:

  • Tổng số ước nguyên dương của x: d(x) = (k₁ + 1) × (k₂ + 1) × ... (với 84: (2 + 1) × (1 + 1) × (1 + 1) = 12 ước).
  • Tổng các ước nguyên dương của x: tích của các cấp số nhân (1 + pᵢ + pᵢ² + ... + pᵢ^kᵢ).
#include <iostream>
#include <vector>
using namespace std;

// Tính ước nguyên tố nhỏ nhất (Smallest Prime Factor — spf) cho mọi số từ 1 đến n
vector<int> taoBangSPF(int n) {
    vector<int> spf(n + 1);
    for (int i = 0; i <= n; ++i) spf[i] = i;
    for (int p = 2; p * p <= n; ++p) {
        if (spf[p] == p) {  // p là số nguyên tố
            for (int m = p * p; m <= n; m += p) {
                if (spf[m] == m) spf[m] = p;
            }
        }
    }
    return spf;
}

// Phân tích x ra thừa số nguyên tố trong O(log x) nhờ bảng spf
vector<int> phanTichThuaSo(int x, const vector<int>& spf) {
    vector<int> thuaSo;
    while (x > 1) {
        thuaSo.push_back(spf[x]);
        x /= spf[x];
    }
    return thuaSo;
}

int main() {
    vector<int> spf = taoBangSPF(1000);
    vector<int> ts = phanTichThuaSo(360, spf);
    for (int i = 0; i < (int)ts.size(); ++i) {
        cout << ts[i] << (i + 1 == (int)ts.size() ? "\n" : " ");  // 2 2 2 3 3 5
    }
    return 0;
}

Một số mẹo tối ưu bộ nhớ và tốc độ cho Sàng Eratosthenes

Khi giới hạn n tiến sát 10⁷ hoặc 10⁸, hai mẹo nhỏ sau đây giúp chương trình của bạn chạy nhanh hơn gấp đôi và tiết kiệm bộ nhớ đáng kể:

  1. Chỉ sàng các số lẻ (Tiết kiệm 50% thời gian): Số 2 là số nguyên tố chẵn duy nhất. Bạn có thể xử lý riêng các số chẵn, sau đó chỉ duyệt p = 3, 5, 7... với bước nhảy p += 2. Trong vòng lặp trong, vì p là số lẻ nên p * p là số lẻ và p * p + p là số chẵn (đã bị loại từ đầu); do đó bước nhảy của vòng trong có thể tăng gấp đôi thành m += 2 * p để chỉ đi qua các bội lẻ của p.
  2. Dùng vector<bool> hoặc bitset<MAXN> trong C++ (Tiết kiệm 8 lần bộ nhớ): Một mảng bool isPrime[10000000] thông thường dùng 1 byte cho mỗi phần tử (tốn ~10 MB), trong khi std::bitset<10000001> nén mỗi phần tử vào đúng 1 bit (chỉ tốn ~1.25 MB), giúp toàn bộ bảng sàng nằm gọn trong bộ nhớ đệm L2/L3 Cache của CPU và tăng tốc độ truy cập bộ nhớ rõ rệt.

Khi nào dùng hàm O(√n), khi nào dùng Sàng Eratosthenes?

Tình huống trong đề bài Cách chọn tối ưu Lý do
Kiểm tra 1 hoặc vài số đơn lẻ, trong đó n rất lớn (n ≤ 10¹² hoặc 10¹⁴) Hàm kiểm tra đơn lẻ O(√n) Không thể tạo mảng sàng kích thước 10¹² vì tràn bộ nhớ RAM (sàng chỉ tạo được tới khoảng n ≤ 10⁷ .. 10⁸)
Liệt kê, đếm hoặc tính tổng các số nguyên tố trong đoạn [1, n] với n ≤ 10⁷ Sàng Eratosthenes O(n log log n) Chạy một lần ở đầu chương trình lấy toàn bộ kết quả
Trả lời q = 10⁵ truy vấn kiểm tra nguyên tố hoặc phân tích thừa số với các số x ≤ 10⁶ Sàng Eratosthenes / Bảng SPF Tiền xử lý một lần đầu, mỗi truy vấn sau đó chỉ mất O(1) (kiểm tra) hoặc O(log x) (phân tích thừa số)

Lỗi hay gặp

Cẩn thận
  • Quên gán isPrime[0] = isPrime[1] = false. Số 0 và số 1 không phải là số nguyên tố. Nếu quên dòng này, khi cộng tổng các số nguyên tố trong đoạn [1, R] kết quả sẽ bị thừa thêm 1.
  • Tràn số int khi tính p * p trong C++. Nếu bạn viết vòng lặp for (int p = 2; p <= n; ++p) rồi bên trong mới viết for (int m = p * p; m <= n; m += p), khi p = 46341 thì p * p > 2³¹ - 1 làm biến m bị tràn sang số âm và gây lỗi truy cập bộ nhớ! Đặt điều kiện dừng ngay ở vòng ngoài for (int p = 2; p * p <= n; ++p) vừa nhanh hơn vừa đảm bảo p * p không bao giờ vượt quá n.
  • Dùng i <= sqrt(n) trong điều kiện vòng lặp C++ mà không cẩn thận sai số thực. Viết i * i <= n (với i kiểu long long khi n lớn) vừa tránh gọi hàm sqrt() ở mỗi bước vừa không bao giờ bị sai số dấu chấm động.

Luyện ngay trên Big-O Coding

Thực hành ngay kiểm tra số nguyên tố, sàng Eratosthenes và kết hợp với mảng cộng dồn qua các bài tập dưới đây trên hệ thống Big-O Coding:

Câu hỏi thường gặp

Số nguyên tố chẵn duy nhất là số mấy?

Là số 2. Mọi số chẵn lớn hơn 2 đều chia hết cho 2 nên đều là hợp số. Vì vậy, khi muốn tối ưu thêm hằng số cho hàm kiểm tra nguyên tố, sau khi kiểm tra riêng n == 2 và loại các số chẵn n % 2 == 0, bạn có thể chỉ duyệt các ước lẻ i = 3, 5, 7... với bước nhảy i += 2.

Nếu đề bài yêu cầu tìm các số nguyên tố trong đoạn [L, R] với L, R lên tới 10¹² nhưng R - L ≤ 10⁶ thì làm thế nào?

Ta dùng kỹ thuật Sàng trên đoạn (Segmented Sieve): đầu tiên sàng thông thường để tìm các số nguyên tố p ≤ √R (tức p ≤ 10⁶), sau đó dùng chính các số nguyên tố p này để gạch bội của chúng nằm trong đoạn [L, R] (dịch chỉ số về 0 .. R - L).

Tổng các số nguyên tố trong đoạn [L, R] với 10⁵ truy vấn tính nhanh bằng cách nào?

Bạn kết hợp hai kỹ thuật: bước 1 chạy Sàng Eratosthenes để biết số nào là nguyên tố; bước 2 tạo một mảng cộng dồn prefPrime mà tại vị trí i cộng thêm i nếu i là số nguyên tố (ngược lại cộng 0). Khi đó mỗi truy vấn [L, R] trả lời ngay trong O(1).

Muốn học bài bản cùng giảng viên?

Khóa Orange: làm chủ thuật toán nâng cao như Graph, DP tối ưu, Segment Tree, HLD để giải bài Hard. 16 buổi online, C++/Java/Python, học thử buổi 1 miễn phí.

Xem Khóa Orange

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483