Mục tiêu:
- Nắm vững các khái niệm và thuật toán Số học cơ bản.
- Hiểu và vận dụng thành thạo các định lý, kỹ thuật nâng cao trong môi trường modulo.
- Nhận diện và giải quyết các bài toán ứng dụng Số học trong các kỳ thi lập trình.
LỜI MỞ ĐẦU
Số học, một trong những nhánh toán học cổ xưa nhất, lại đóng một vai trò quan trọng và đầy bất ngờ trong thế giới Tin học hiện đại, đặc biệt là trong lĩnh vực lập trình thi đấu (Competitive Programming). Từ các kỳ thi học sinh giỏi quốc gia (HSG QG), các vòng loại khu vực ICPC cho đến các kỳ thi online trên Codeforces hay AtCoder, các bài toán liên quan đến Số học luôn xuất hiện với tần suất đáng kể.
Đôi khi, Số học là công cụ chính để tối ưu hóa thuật toán, biến một lời giải "trâu bò" hay thành một lời giải thanh lịch hoặc . Ví dụ kinh điển là việc tính tổng một cấp số cộng: thay vì dùng vòng lặp, ta áp dụng công thức toán học. Trong các trường hợp khác, Số học lại là trái tim của bài toán, đòi hỏi thí sinh phải có kiến thức sâu về các định lý, các thuật toán kiểm tra số nguyên tố, phân tích thừa số, hay xử lý các phép toán trên các số cực lớn thông qua đồng dư thức.
Chuyên đề này được biên soạn nhằm cung cấp một lộ trình học tập có hệ thống, đi từ những viên gạch nền tảng nhất như Thuật toán Euclid, Sàng Eratosthenes cho đến các công cụ mạnh mẽ như Nghịch đảo Modulo, Định lý Euler, Định lý Thặng dư Trung Hoa và các ứng dụng của chúng trong bài toán tổ hợp. Lộ trình học tập được đề xuất như sau:
- Nắm vững Phần I: Đây là những kiến thức bắt buộc, là nền tảng cho mọi chủ đề sau này.
- Làm chủ Phần II: Đây là phần cốt lõi của Số học thi đấu, đặc biệt là các kỹ thuật trong môi trường modulo.
- Luyện tập với Phần III: Vận dụng lý thuyết vào các bài toán kinh điển để xây dựng kỹ năng nhận diện và giải quyết vấn đề.
- Thử thách bản thân với Phần IV: Rèn luyện với các bài tập từ dễ đến khó trên các nền tảng thực tế.
Với sự kiên trì và một phương pháp học tập đúng đắn, bạn hoàn toàn có thể làm chủ chuyên đề quan trọng này và biến Số học thành một lợi thế của mình trong các kỳ thi sắp tới.
PHẦN I: NỀN TẢNG SỐ HỌC CƠ BẢN (FOUNDATION)
Mục tiêu: Xây dựng nền tảng vững chắc, nắm bắt các công cụ cốt lõi.
A. Ước chung lớn nhất (GCD) và Bội chung nhỏ nhất (LCM)
-
Định nghĩa:
- Ước số (Divisor): Số nguyên
alà ước của số nguyênbnếubchia hết choa(ký hiệua | b). - Bội số (Multiple): Số nguyên
blà bội của số nguyênanếublà ước củaa. - Ước chung (Common Divisor): Số nguyên
dlà ước chung củaavàbnếudvừa là ước củaa, vừa là ước củab. - Ước chung lớn nhất (Greatest Common Divisor - GCD): Là số nguyên dương lớn nhất trong tập hợp các ước chung của
avàb. Ký hiệu:gcd(a, b)hoặc(a, b).
- Ước số (Divisor): Số nguyên
-
Thuật toán Euclid tìm GCD:
- Ý tưởng: Dựa trên nhận xét rằng một số là ước chung của
avàbkhi và chỉ khi nó cũng là ước chung củabvà phần dưa % b. Do đó, ta có tính chất quan trọng:Quá trình này lặp lại cho đến khi một trong hai số bằng 0, khi đó GCD chính là số còn lại.
- Chứng minh tính đúng đắn (trực quan): Gọi
d = gcd(a, b). Khi đóavàbđều chia hết chod. Vìa = q*b + (a % b), nêna % b = a - q*b. Doavàq*bđều chia hết chod,a % bcũng chia hết chod. Vậydlà ước chung củabvàa % b. Ta có thể chứng minh tương tự theo chiều ngược lại, do đó tập ước chung của(a, b)và(b, a % b)là như nhau, dẫn đến GCD của chúng cũng bằng nhau. - Cài đặt:
- Đệ quy:
int gcd_recursive(int a, int b) { if (b == 0) { return a; } return gcd_recursive(b, a % b); } - Không đệ quy (Lặp):
int gcd_iterative(int a, int b) { while (b) { a %= b; std::swap(a, b); } return a; }
- Đệ quy:
- Độ phức tạp: Độ phức tạp của thuật toán Euclid là . Theo Định lý Lamé, số bước của thuật toán không bao giờ vượt quá 5 lần số chữ số của số nhỏ hơn (trong hệ cơ số 10).
- Hàm
std::gcdtrong C++17: Từ C++17, có thể dùng hàmstd::gcdtrong thư viện<numeric>.#include <iostream> #include <numeric> // Required for std::gcd int main() { int a = 54, b = 24; std::cout << "GCD of " << a << " and " << b << " is " << std::gcd(a, b) << std::endl; return 0; }
- Ý tưởng: Dựa trên nhận xét rằng một số là ước chung của
-
Bội chung nhỏ nhất (LCM):
- Công thức tính thông qua GCD:
- Lưu ý về tràn số: Phép nhân
a * bcó thể gây tràn số đối với các kiểu dữ liệu cơ bản (int,long long) trước khi thực hiện phép chia. Để tránh điều này, ta nên cài đặt công thức một cách an toàn hơn: - Code mẫu:
long long lcm(long long a, long long b) { if (a == 0 || b == 0) return 0; return (a / std::gcd(a, b)) * b; }
- Công thức tính thông qua GCD:
B. Số Nguyên Tố và Phân Tích Thừa Số Nguyên Tố
-
Định nghĩa:
- Số nguyên tố (Prime Number): Là số tự nhiên lớn hơn 1 và chỉ có hai ước số dương là 1 và chính nó.
- Hợp số (Composite Number): Là số tự nhiên lớn hơn 1 không phải là số nguyên tố.
- Định lý cơ bản của Số học (Fundamental Theorem of Arithmetic): Mọi số nguyên lớn hơn 1 đều có thể được biểu diễn một cách duy nhất (không kể thứ tự) dưới dạng tích của các số nguyên tố. Ví dụ: .
-
Kiểm tra tính nguyên tố (Primality Test):
- Phương pháp duyệt ngây thơ
O(n): Duyệt tất cả các số từ 2 đếnn-1, nếu có số nào là ước củanthìnlà hợp số. - Phương pháp tối ưu: Duyệt đến
sqrt(n):- Giải thích: Nếu
nlà hợp số, nó có thể được viết dưới dạngn = a * b. Nếu cảavàbđều lớn hơnsqrt(n), thìa * b > sqrt(n) * sqrt(n) = n, vô lý. Do đó, ít nhất một trong hai ướcahoặcbphải nhỏ hơn hoặc bằngsqrt(n). Ta chỉ cần duyệt tìm ước đếnsqrt(n).
- Giải thích: Nếu
- Code mẫu và phân tích độ phức tạp:
bool is_prime(int n) { if (n <= 1) return false; for (int i = 2; i * i <= n; ++i) { if (n % i == 0) { return false; } } return true; } // Độ phức tạp: O(sqrt(n))
- Phương pháp duyệt ngây thơ
-
Phân tích thừa số nguyên tố (Prime Factorization):
- Phương pháp duyệt đến
sqrt(n): Ta lặp qua các sốitừ 2 đếnsqrt(n). Nếuilà ước củan, ta đếm xemixuất hiện bao nhiêu lần trong phân tích và chianchoiđến khi không chia hết nữa. Sau vòng lặp, nếunvẫn còn lớn hơn 1, thì phần còn lại củanchính là một thừa số nguyên tố. - Code mẫu và phân tích độ phức tạp:
#include <vector> #include <map> // Phân tích ra map<thừa số, số mũ> std::map<int, int> factorize(int n) { std::map<int, int> factors; for (int i = 2; i * i <= n; ++i) { while (n % i == 0) { factors[i]++; n /= i; } } if (n > 1) { factors[n]++; } return factors; } // Độ phức tạp: O(sqrt(n))
- Phương pháp duyệt đến
-
Sàng Eratosthenes (Sieve of Eratosthenes):
-
Mục đích: Tìm tất cả các số nguyên tố nhỏ hơn hoặc bằng
Nmột cách hiệu quả. -
Ý tưởng: Bắt đầu với một danh sách tất cả các số từ 2 đến
N. Lần lượt lấy số nguyên tốpnhỏ nhất chưa được xét. Đánh dấu tất cả các bội củap(như2p, 3p, 4p, ...) là hợp số. Lặp lại quá trình này với số nguyên tố tiếp theo. Những số không bị đánh dấu cuối cùng là số nguyên tố. Một tối ưu quan trọng là bắt đầu đánh dấu từp*p, vì các bội nhỏ hơn củap(như2p, 3p) đã được đánh dấu bởi các số nguyên tố nhỏ hơnp(như 2, 3). -
Cài đặt sàng cơ bản:
#include <vector> std::vector<bool> sieve_eratosthenes(int N) { std::vector<bool> is_prime(N + 1, true); is_prime[0] = is_prime[1] = false; for (int p = 2; p * p <= N; ++p) { if (is_prime[p]) { for (int i = p * p; i <= N; i += p) is_prime[i] = false; } } return is_prime; } -
Độ phức tạp: .
-
Biến thể nâng cao:
- Sàng tuyến tính (Linear Sieve): Thuật toán này đảm bảo mỗi hợp số chỉ bị đánh dấu đúng một lần bởi ước nguyên tố nhỏ nhất của nó. Điều này giúp giảm độ phức tạp xuống còn .
- Sàng tìm ước nguyên tố nhỏ nhất (Smallest Prime Factor - SPF): Biến thể này không chỉ đánh dấu số nguyên tố/hợp số mà còn lưu lại ước nguyên tố nhỏ nhất (SPF) của mỗi số. Sau khi sàng xong, ta có thể phân tích thừa số nguyên tố của bất kỳ số
n <= Ntrong thời gian bằng cách liên tục chianchospf[n].
// Sàng tuyến tính kết hợp tìm SPF #include <vector> const int MAXN = 1000000; std::vector<int> primes; int spf[MAXN + 1]; void linear_sieve(int N) { for (int i = 2; i <= N; ++i) { if (spf[i] == 0) { spf[i] = i; primes.push_back(i); } for (int p : primes) { if (p > spf[i] || (long long)i * p > N) { break; } spf[i * p] = p; } } } // Phân tích T_S_N_T nhanh sau khi sàng std::map<int, int> fast_factorize(int n) { std::map<int, int> factors; while (n != 1) { factors[spf[n]]++; n /= spf[n]; } return factors; } // Độ phức tạp sàng: O(N) // Độ phức tạp mỗi truy vấn phân tích: O(log n)
-
PHẦN II: SỐ HỌC NÂNG CAO VÀ ĐỒNG DƯ THỨC (MODULAR ARITHMETIC)
Mục tiêu: Trang bị các công cụ mạnh để giải quyết các bài toán với số lớn.
A. Đồng dư thức (Modular Arithmetic)
-
Định nghĩa: Hai số nguyên
avàbđược gọi là đồng dư với nhau theo modulom(vớimlà số nguyên dương) nếu chúng có cùng số dư khi chia chom. Ký hiệu:Điều này tương đương với
(a - b)chia hết chom. -
Các phép toán cơ bản:
- Cộng:
- Trừ: (Cộng thêm
mđể đảm bảo kết quả không âm). - Nhân:
-
Lũy thừa nhanh (Binary Exponentiation / Exponentiation by Squaring):
- Mục đích: Tính trong .
- Ý tưởng: Dựa trên biểu diễn nhị phân của số mũ
b.- Nếu
blà số chẵn, . - Nếu
blà số lẻ, . Ta lặp lại quá trình này, giảmbđi một nửa ở mỗi bước, cho đến khib = 0.
- Nếu
- Code mẫu:
long long power(long long base, long long exp, long long mod) { long long res = 1; base %= mod; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % mod; base = (base * base) % mod; exp /= 2; } return res; }
B. Nghịch đảo Modulo (Modular Inverse)
-
Định nghĩa: Nghịch đảo modulo của
atheo modulomlà một số nguyênxsao cho:Ký hiệu: . Phép chia
(a / b) mod mđược thực hiện bằng cách tính(a * b⁻¹) mod m. -
Điều kiện tồn tại: Nghịch đảo modulo của
atheomtồn tại khi và chỉ khiavàmlà hai số nguyên tố cùng nhau, tức là . -
Phương pháp tìm nghịch đảo:
-
Khi
mlà số nguyên tố: Dùng Định lý Fermat nhỏ: Nếumlà số nguyên tố vàakhông chia hết chom, ta có:Suy ra:
Vậy . Ta có thể tính bằng Lũy thừa nhanh.
-
Khi
mlà hợp số (trường hợp tổng quát): Dùng Thuật toán Euclid mở rộng (Extended Euclidean Algorithm).- Ý tưởng: Thuật toán này tìm cặp số nguyên
(x, y)sao cho:Nếu , ta có . Lấy modulo
mcả hai vế:Khi đó,
xchính là nghịch đảo modulo củaa. Giá trịxtrả về có thể âm, ta cần chuẩn hóa nó về khoảng[1, m-1]bằng cách(x % m + m) % m. - Code mẫu cho Euclid mở rộng:
// Trả về gcd(a, b) và gán giá trị cho x, y long long extended_gcd(long long a, long long b, long long &x, long long &y) { if (a == 0) { x = 0; y = 1; return b; } long long x1, y1; long long d = extended_gcd(b % a, a, x1, y1); x = y1 - (b / a) * x1; y = x1; return d; } // Tìm nghịch đảo modulo long long mod_inverse(long long a, long long m) { long long x, y; long long g = extended_gcd(a, m, x, y); if (g != 1) { // Nghịch đảo không tồn tại return -1; } return (x % m + m) % m; }
- Ý tưởng: Thuật toán này tìm cặp số nguyên
-
C. Các Định Lý Quan Trọng
-
Định lý Fermat nhỏ (Fermat's Little Theorem): Đã phát biểu và ứng dụng ở trên.
-
Hàm phi Euler (Euler's Totient Function):
- Định nghĩa: (phi của n) là số các số nguyên dương nhỏ hơn hoặc bằng
nvà nguyên tố cùng nhau vớin. Ví dụ: vì có 6 số {1, 2, 4, 5, 7, 8} nguyên tố cùng nhau với 9. - Công thức tính: Nếu phân tích thừa số nguyên tố của
nlà , thì:
- Định nghĩa: (phi của n) là số các số nguyên dương nhỏ hơn hoặc bằng
-
Định lý Euler (Euler's Totient Theorem):
- Phát biểu: Nếu , thì:
- Đây là sự tổng quát hóa của Định lý Fermat nhỏ. Khi
mlà số nguyên tốp, thì , và định lý Euler trở thành định lý Fermat nhỏ. Định lý này cho phép tìm nghịch đảo modulo củaatheom(kể cả khimlà hợp số) bằng công thức , nhưng yêu cầu phải tính được .
- Phát biểu: Nếu , thì:
D. Phương trình Diophantine tuyến tính
-
Dạng phương trình: , với
a, b, clà các số nguyên đã biết, và cần tìm nghiệm nguyên(x, y). -
Định lý Bezout: Phương trình có nghiệm nguyên
(x, y)khi và chỉ khicchia hết chod = gcd(a, b). -
Cách tìm một nghiệm:
- Sử dụng thuật toán Euclid mở rộng để tìm
x_0, y_0cho phương trình . - Nếu
cchia hết chod, thì một nghiệm riêng(x', y')của phương trình ban đầu là:
- Sử dụng thuật toán Euclid mở rộng để tìm
-
Cách tìm tất cả các nghiệm: Từ một nghiệm riêng
(x', y'), tập hợp tất cả các nghiệm của phương trình được cho bởi công thức:với
klà một số nguyên bất kỳ.
PHẦN III: CÁC BÀI TOÁN ỨNG DỤNG KINH ĐIỂN
Mục tiêu: Rèn luyện kỹ năng nhận diện và áp dụng lý thuyết vào giải toán.
A. Bài toán về Ước số
-
Đếm số lượng ước của một số
n:- Dựa trên phân tích thừa số nguyên tố .
- Một ước bất kỳ của
nsẽ có dạng với . - Với mỗi , số mũ có thể có cách chọn (từ 0 đến ).
- Công thức: Số lượng ước, ký hiệu hoặc :
-
Tính tổng các ước của một số
n:- Công thức: Tổng các ước, ký hiệu :
Mỗi tổng trong ngoặc là tổng của một cấp số nhân: .
- Công thức: Tổng các ước, ký hiệu :
B. Bài toán Tổ hợp liên quan đến chia hết
-
Tính Tổ hợp chập
kcủan() modulom:- Trường hợp
mlà số nguyên tố:- Công thức: .
- Tính toán: Ta không thể thực hiện phép chia trực tiếp trong môi trường modulo. Thay vào đó, ta chuyển nó thành phép nhân với nghịch đảo modulo:
- Yêu cầu: Cần tính trước (precompute) các giá trị giai thừa
n!và nghịch đảo modulo của các giai thừa(n!)⁻¹theo modulom. Nghịch đảo có thể tính bằng Định lý Fermat nhỏ vìmlà số nguyên tố.const int MAXN = 100000; const int MOD = 1e9 + 7; long long fact[MAXN + 1]; long long invFact[MAXN + 1]; // Hàm lũy thừa nhanh power() đã định nghĩa ở trên long long modInverse(long long n) { return power(n, MOD - 2, MOD); } void precompute() { fact[0] = 1; for (int i = 1; i <= MAXN; i++) { fact[i] = (fact[i - 1] * i) % MOD; } invFact[MAXN] = modInverse(fact[MAXN]); for (int i = MAXN - 1; i >= 0; i--) { invFact[i] = (invFact[i + 1] * (i + 1)) % MOD; } } long long nCk_mod_p(int n, int k) { if (k < 0 || k > n) return 0; return (((fact[n] * invFact[k]) % MOD) * invFact[n - k]) % MOD; }
- Trường hợp
mlà hợp số:- Định lý Lucas: Khi cần tính với
plà số nguyên tố vàn, klớn, Định lý Lucas cho phép chia nhỏ bài toán. Nếu biểu diễn củanvàktrong cơ sốplà và , thì: - Khi
mlà hợp số: Ta phân tíchmthành các thừa số nguyên tố dạng . Ta tính theo từng modulo . Cuối cùng, dùng Định lý Thặng dư Trung Hoa (Chinese Remainder Theorem - CRT) để tìm ra kết quả cuối cùng theo modulom. Việc tính phức tạp hơn và thường yêu cầu xử lý cẩn thận các thừa sốptrong giai thừa.
- Định lý Lucas: Khi cần tính với
- Trường hợp
-
Các bài toán đếm khác: Nhiều bài toán đếm trong quy hoạch động hoặc tổ hợp có các ràng buộc chia hết. Ví dụ: "Đếm số dãy con có tổng chia hết cho K", "Đếm số cách tô màu một đồ thị sao cho số đỉnh màu X chia hết cho 3". Các bài toán này thường được giải quyết bằng cách mở rộng trạng thái của quy hoạch động để lưu thêm thông tin về số dư
(state, remainder).
PHẦN IV: BÀI TẬP LUYỆN TẬP
-
Nhóm 1: Nhận biết - Thông hiểu (Cơ bản)
- Bài tập chỉ yêu cầu cài đặt trực tiếp các thuật toán đã học.
- Ví dụ:
- Codeforces - 791A (Bear and Big Brother) - Bài tập làm quen
- Codeforces - 231A (Team) - Bài tập làm quen
- "Cho A, B, M, tính " (Lũy thừa nhanh).
- "Cho N, tìm tất cả các số nguyên tố không vượt quá N" (Sàng Eratosthenes).
- "Cho hai số A, B, tìm ước chung lớn nhất và bội chung nhỏ nhất" (GCD, LCM).
-
Nhóm 2: Vận dụng (Trung bình)
- Bài tập yêu cầu kết hợp 1-2 khái niệm hoặc biến đổi nhẹ.
- Ví dụ:
- Codeforces - 271A (Beautiful Year) - Duyệt và kiểm tra tính chất
- Codeforces - 472A (Design Tutorial: Learn from Math) - Sàng, tìm cặp số
- "Tính " (Tổ hợp modulo số nguyên tố).
- "Đếm số ước của N!" (Dùng công thức Legendre).
- "Tìm nghiệm nguyên dương nhỏ nhất của phương trình ax + by = c" (Phương trình Diophantine).
- Codeforces - 1352C (K-th Not Divisible by n) - Phân tích toán học, tìm kiếm nhị phân.
-
Nhóm 3: Vận dụng cao (Nâng cao)
- Bài toán kết hợp Số học với các chuyên đề khác (Quy hoạch động, Đồ thị, ...).
- Bài toán đòi hỏi nhận xét toán học sâu sắc, sử dụng các định lý, thuật toán nâng cao.
- Ví dụ:
- "Đếm số dãy con có tổng chia hết cho K" (Quy hoạch động với modulo).
- Codeforces - 1108D (Diverse Garland) - Quy hoạch động đơn giản
- Các bài toán yêu cầu dùng Định lý Lucas, CRT, hoặc Miller-Rabin, Pollard's Rho.
- Các bài toán đếm phức tạp sử dụng công thức đảo ngược Möbius.
- AtCoder DP Contest - G (Longest Path) - Quy hoạch động trên đồ thị.
-
Nguồn bài tập gợi ý:
- VNOI:
wiki.vnoi.infovàoj.vnoi.info - Codeforces:
codeforces.com/problemset(lọc theo tagnumber theory,math,combinatorics) - AtCoder:
atcoder.jp/contests(đặc biệt là các kỳ thiBeginnervàRegular) - Topcoder:
topcoder.com/community/data-science/data-science-tutorials(các bài viết hướng dẫn chất lượng cao).
- VNOI:
PHỤ LỤC
A. Code Template
Dưới đây là mã nguồn C++ sạch, có chú thích cho các thuật toán quan trọng.
#include <iostream>
#include <vector>
#include <numeric> // For std::gcd in C++17
#include <map>
// --- PHẦN I: CƠ BẢN ---
// Thuật toán Euclid lặp
long long gcd(long long a, long long b) {
while (b) {
a %= b;
std::swap(a, b);
}
return a;
}
// Bội chung nhỏ nhất
long long lcm(long long a, long long b) {
if (a == 0 || b == 0) return 0;
return std::abs(a * b) / gcd(a, b);
// An toàn hơn: return (a / gcd(a, b)) * b;
}
// Kiểm tra số nguyên tố O(sqrt(n))
bool is_prime(long long n) {
if (n <= 1) return false;
for (long long i = 2; i * i <= n; ++i) {
if (n % i == 0) return false;
}
return true;
}
// Sàng tìm ước nguyên tố nhỏ nhất (SPF)
// Cần gọi linear_sieve(MAXN) trước khi dùng
const int MAXN_SIEVE = 1000001;
std::vector<int> primes;
int spf[MAXN_SIEVE];
void linear_sieve(int N) {
for (int i = 2; i <= N; ++i) {
if (spf[i] == 0) {
spf[i] = i;
primes.push_back(i);
}
for (int p : primes) {
if (p > spf[i] || (long long)i * p > N) break;
spf[i * p] = p;
}
}
}
// --- PHẦN II: NÂNG CAO ---
// Lũy thừa nhanh (Binary Exponentiation)
long long power(long long base, long long exp, long long mod) {
long long res = 1;
base %= mod;
while (exp > 0) {
if (exp % 2 == 1) res = (__int128)res * base % mod;
base = (__int128)base * base % mod;
exp /= 2;
}
return res;
}
// Thuật toán Euclid mở rộng
// Trả về gcd(a, b) và gán giá trị cho x, y thỏa mãn ax + by = gcd(a, b)
long long extended_gcd(long long a, long long b, long long &x, long long &y) {
if (a == 0) {
x = 0;
y = 1;
return b;
}
long long x1, y1;
long long d = extended_gcd(b % a, a, x1, y1);
x = y1 - (b / a) * x1;
y = x1;
return d;
}
// Nghịch đảo modulo
long long mod_inverse(long long a, long long m) {
long long x, y;
long long g = extended_gcd(a, m, x, y);
if (g != 1) return -1; // Nghịch đảo không tồn tại
return (x % m + m) % m;
}
// --- PHẦN III: ỨNG DỤNG ---
// Tổ hợp C(n, k) modulo số nguyên tố p
// Cần gọi precompute() trước khi dùng
const int MAXN_COMB = 100001;
const int MOD = 1e9 + 7;
long long fact[MAXN_COMB];
long long invFact[MAXN_COMB];
void precompute_factorials() {
fact[0] = 1;
for (int i = 1; i < MAXN_COMB; i++) {
fact[i] = (fact[i - 1] * i) % MOD;
}
invFact[MAXN_COMB - 1] = power(fact[MAXN_COMB - 1], MOD - 2, MOD);
for (int i = MAXN_COMB - 2; i >= 0; i--) {
invFact[i] = (invFact[i + 1] * (i + 1)) % MOD;
}
}
long long nCk_mod_p(int n, int k) {
if (k < 0 || k > n) return 0;
return (((fact[n] * invFact[k]) % MOD) * invFact[n - k]) % MOD;
}
B. Bảng Tóm Tắt
| Thuật toán / Công thức | Mục đích | Độ phức tạp | Lưu ý |
|---|---|---|---|
| Thuật toán Euclid | Tìm GCD của hai số a, b |
Nền tảng cho nhiều thuật toán khác. | |
| Kiểm tra nguyên tố | Kiểm tra số n có phải số nguyên tố không |
Chỉ hiệu quả với n không quá lớn (khoảng ). |
|
| Phân tích T_S_N_T | Phân tích n ra thừa số nguyên tố |
Tương tự, hiệu quả với n không quá lớn. |
|
| Sàng Eratosthenes | Tìm tất cả số nguyên tố | Hiệu quả cho . | |
| Sàng tuyến tính & SPF | Tìm tất cả số nguyên tố / Tìm SPF của mọi số | Tối ưu hơn Sàng Eratosthenes về tiệm cận. SPF giúp phân tích T_S_N_T nhanh. | |
| Lũy thừa nhanh | Tính | Cực kỳ quan trọng, ứng dụng rộng rãi. | |
| Euclid mở rộng | Tìm x, y trong ax + by = gcd(a, b) |
Dùng để tìm nghịch đảo modulo và giải phương trình Diophantine. | |
| Nghịch đảo Modulo (Fermat) | Tìm | Chỉ áp dụng khi m là số nguyên tố. |
|
| Nghịch đảo Modulo (Euclid M_R) | Tổng quát, áp dụng cho cả m là hợp số. |
||
| Tổ hợp | Tính tổ hợp modulo số nguyên tố p |
Precompute: , Query: | Yêu cầu n, k không quá lớn, p là số nguyên tố. |
| Kiểm tra nguyên tố Miller-Rabin | Kiểm tra n lớn có phải số nguyên tố không (xác suất) |
Tiêu chuẩn cho n lớn (đến ), với k là số lần lặp. |