- Hai số nguyên
avàbđược gọi là đồng dư theo moduloM(ký hiệua ≡ b (mod M)) khi hiệua - bchia hết choM. - Hằng số
MOD = 10⁹ + 7(1000000007) được ưa chuộng vì:2 × MODkhông trànint32-bit,MOD × MODkhông trànlong long64-bit, vàMODlà số nguyên tố nên mọi số từ1đếnMOD - 1đều có nghịch đảo modulo. - Trong C++, phần dư của số âm có thể âm (
-7 % 3 == -1); vì vậy khi trừ đồng dư bắt buộc phải viết(a % M - b % M + M) % M. - Không được chia trực tiếp
(a / b) % M: phải biến đổi phép chia thành phép nhân với nghịch đảo modulo củab(a * b^(M - 2) % MkhiMnguyên tố).
Vì sao các bài toán thuật toán luôn bắt chia lấy dư cho 10^9 + 7?
Bạn sẽ thường xuyên gặp câu yêu cầu ở cuối đề bài: “Vì kết quả có thể rất lớn, hãy in ra phần dư của đáp án khi chia cho 10⁹ + 7 (tức 1.000.000.007).”
Hãy xét bài toán đếm cơ bản: tính giai thừa 100! (số cách xếp 100 người thành hàng ngang). Con số 100! dài tới 158 chữ số, vượt xa giới hạn của kiểu unsigned long long 64-bit trong C++ (chỉ lưu được khoảng 20 chữ số). Thay vì bắt cài đặt phép nhân số lớn (BigInt) cồng kềnh, việc lấy kết quả theo modulo M = 10⁹ + 7 mang lại 3 lợi ích thiết thực:
- Tổng của hai số dư không tràn kiểu
int32-bit có dấu: Mọi số dư khi chia cho10⁹ + 7đều nằm trong đoạn[0, 10⁹ + 6]. Tổng hai số dư tối đa là2 × 10⁹ + 12, nhỏ hơn giới hạn2³¹ - 1 ≈ 2,147 × 10⁹của kiểuint. - Tích của hai số dư không tràn kiểu
long long64-bit: Khi nhân hai số dư, tích tối đa là(10⁹ + 6)² ≈ 10¹⁸, nằm gọn bên dưới trần2⁶³ - 1 ≈ 9,22 × 10¹⁸của kiểulong long. 10⁹ + 7là một số nguyên tố: KhiMnguyên tố, mọi số từ1đếnM - 1đều có nghịch đảo phép nhân, cho phép thực hiện phép chia đồng dư (như khi tính tổ hợpC(n, k)).
Ba công thức đồng dư nền tảng: Cộng, Nhân và Trừ
Hình ảnh trực quan nhất của số học đồng dư là chiếc đồng hồ 12 giờ: khi kim giờ chỉ số 10, cộng thêm 5 tiếng sẽ quay vòng về số 3 (vì 15 % 12 = 3). Với số nguyên dương M bất kỳ, phép cộng, trừ và nhân bảo toàn tính đồng dư: bạn có thể lấy phần dư ở từng bước trung gian trước khi thực hiện phép tính tiếp theo để hạn chế độ lớn của giá trị; bản thân phép nhân trước % vẫn có thể tràn nếu kiểu dữ liệu không đủ rộng.
| Phép toán | Công thức toán học | Cách viết an toàn trong C++ (với M = 10⁹ + 7) |
|---|---|---|
Chuẩn hóa x bất kỳ |
x mod M ∈ [0, M - 1] |
((x % M) + M) % M |
| Phép cộng | (a + b) mod M |
((a % M) + (b % M)) % M |
| Phép trừ | (a - b) mod M |
((a % M) - (b % M) + M) % M |
| Phép nhân | (a × b) mod M |
(1LL * (a % M) * (b % M)) % M |
Trong ba dòng cộng, trừ và nhân của bảng, giả định a, b đã chuẩn hóa về [0, M - 1]; nếu đầu vào có thể âm, chuẩn hóa từng số bằng dòng đầu trước. Các phép tính dùng long long và M = 10⁹ + 7, không áp dụng nguyên xi cho modulo gần giới hạn 64-bit.
Lưu ý trong C++: nếu a và b có kiểu int, tích a * b sẽ tính trên int 32-bit trước rồi mới chia % M, gây tràn số nếu tích vượt 2 × 10⁹. Bạn bắt buộc phải dùng kiểu long long hoặc nhân thêm 1LL * ở đầu biểu thức để ép tính tích trên 64-bit.
Sự khác biệt nguy hiểm giữa C++ và Python khi chia lấy dư số âm
Tại sao trong phép trừ đồng dư, ta không viết (a % M - b % M) % M mà phải cộng bù thêm + M ở trong ngoặc?
Giả sử với M = 10⁹ + 7, ta cần tính (a - b) mod M khi a = 10¹⁸ (có a % M = 49) và b = 100 (có b % M = 100). Dù hiệu thực 10¹⁸ - 100 là số dương, nhưng hiệu hai phần dư lại là số âm: 49 - 100 = -51. Khi chia lấy dư một số âm (như -7 % 3), C++ và Python dùng hai quy tắc trái ngược nhau:
| Ngôn ngữ | Quy tắc làm tròn thương q |
Thương của -7 chia 3 |
Phần dư r = a - q × m của -7 % 3 |
Nhận xét |
|---|---|---|---|---|
| C++, Java | Làm tròn về phía số 0 | -7 / 3 = -2 |
-7 - (-2) × 3 = -1 |
Mang dấu của số bị chia (bị âm!) |
| Python | Làm tròn xuống âm vô cùng | -7 // 3 = -3 |
-7 - (-3) × 3 = 2 |
Luôn thuộc đoạn [0, m - 1] |
Vì đáp án chuẩn luôn đòi hỏi số dư nằm trong [0, M - 1], việc để C++ in ra số âm sẽ bị chấm Sai kết quả (WA). Để triệt tiêu lỗi này:
- Khi chuẩn hóa số nguyên
xbất kỳ (có thể âm): viết((x % M) + M) % M. Vớix = -7, M = 3:-7 % 3ra-1, cộng3thành2, rồi2 % 3 = 2. - Khi trừ hai số dư
a, bđã nằm trong[0, M - 1]: vì hiệua - bchỉ âm xuống tối đa-(M - 1), chỉ cần cộng bù thêm+ M:long long hieu = (a - b + M) % M;.
#include <iostream>
using namespace std;
const long long MOD = 1'000'000'007LL;
// Chuẩn hóa số nguyên x bất kỳ (kể cả số âm) về đoạn [0, m - 1]
long long mod_norm(long long x, long long m = MOD) {
return ((x % m) + m) % m;
}
// Cộng đồng dư: (a + b) mod m
long long mod_add(long long a, long long b, long long m = MOD) {
return (mod_norm(a, m) + mod_norm(b, m)) % m;
}
// Trừ đồng dư an toàn không bao giờ bị âm: (a - b + m) mod m
long long mod_sub(long long a, long long b, long long m = MOD) {
return (mod_norm(a, m) - mod_norm(b, m) + m) % m;
}
// Nhân đồng dư ép kiểu 64-bit tránh tràn số: (a * b) mod m
long long mod_mul(long long a, long long b, long long m = MOD) {
return (mod_norm(a, m) * mod_norm(b, m)) % m;
}
int main() {
// Test 1: Chuẩn hóa số âm -7 khi chia lấy dư cho 3 (kết quả chuẩn = 2)
cout << "Norm(-7, 3): " << mod_norm(-7, 3) << "\n";
// Test 2: Cộng, trừ, nhân hai số lớn sát ngưỡng 10^9 + 7
long long a = 1'000'000'005LL; // Đồng dư với -2 (mod 10^9 + 7)
long long b = 1'000'000'006LL; // Đồng dư với -1 (mod 10^9 + 7)
cout << "Add: " << mod_add(a, b)
<< " | Sub: " << mod_sub(a, b)
<< " | Mul: " << mod_mul(a, b) << "\n";
// Test 3: Tính biểu thức (10^9 * 10^9 - 100) mod (10^9 + 7)
// 10^9 * 10^9 đồng dư (-7) * (-7) = 49; trừ đi 100 rồi cộng MOD ra 999999956
long long tich = mod_mul(1'000'000'000LL, 1'000'000'000LL);
cout << "Expr: " << mod_sub(tich, 100) << "\n";
return 0;
}
MOD = 1_000_000_007
def mod_norm(x: int, m: int = MOD) -> int:
"""Chuẩn hóa số nguyên x bất kỳ về đoạn [0, m - 1]."""
return ((x % m) + m) % m
def mod_add(a: int, b: int, m: int = MOD) -> int:
"""Cộng đồng dư: (a + b) mod m."""
return (mod_norm(a, m) + mod_norm(b, m)) % m
def mod_sub(a: int, b: int, m: int = MOD) -> int:
"""Trừ đồng dư an toàn: (a - b + m) mod m."""
return (mod_norm(a, m) - mod_norm(b, m) + m) % m
def mod_mul(a: int, b: int, m: int = MOD) -> int:
"""Nhân đồng dư: (a * b) mod m."""
return (mod_norm(a, m) * mod_norm(b, m)) % m
if __name__ == "__main__":
# Test 1: Chuẩn hóa số âm -7 khi chia lấy dư cho 3 (kết quả chuẩn = 2)
print(f"Norm(-7, 3): {mod_norm(-7, 3)}")
# Test 2: Cộng, trừ, nhân hai số lớn sát ngưỡng 10^9 + 7
a = 1_000_000_005 # Đồng dư với -2 (mod 10^9 + 7)
b = 1_000_000_006 # Đồng dư với -1 (mod 10^9 + 7)
print(f"Add: {mod_add(a, b)} | Sub: {mod_sub(a, b)} | Mul: {mod_mul(a, b)}")
# Test 3: Tính biểu thức (10^9 * 10^9 - 100) mod (10^9 + 7)
tich = mod_mul(1_000_000_000, 1_000_000_000)
print(f"Expr: {mod_sub(tich, 100)}")
Chủ đề này nằm trong Lecture 06: Number Theory I (Lý thuyết số I) của Khóa Orange. Xem đề cương
Phép chia đồng dư và Nghịch đảo Modulo (Modular Inverse)
Khác với cộng, trừ và nhân, phép chia KHÔNG phân phối qua phép modulo. Ví dụ với a = 36, b = 12, M = 7:
- Chia trước rồi lấy dư:
(36 / 12) % 7 = 3 % 7 = 3(đúng). - Lấy dư từng số trước:
36 % 7 = 1và12 % 7 = 5; phép chia nguyên1 / 5ra0(sai hoàn toàn!).
Để thực hiện phép chia (a / b) mod M, ta phải biến phép chia cho b thành phép nhân với nghịch đảo modulo b⁻¹:
- Số
xtrong[1, M - 1]được gọi là nghịch đảo modulo củabtheo moduloMnếu(b × x) % M == 1. - Khi đó thương trong số học modulo là
a × b⁻¹ mod M, với điều kiệngcd(b, M) = 1. Nó không phải phép chia nguyên làm tròn xuống: chẳng hạn1 / 2trong số học modulo 7 là 4, còn1 // 2là 0. Nếuachia hết chobtrên số nguyên và nghịch đảo tồn tại thì hai cách mới khớp theo modulo.
Cách tìm nghịch đảo modulo b⁻¹:
- Khi
Mlà số nguyên tố (như10⁹ + 7): Theo Định lý Fermat nhỏ, với mọibkhông chia hết choM, ta cób^(M - 1) ≡ 1 (mod M), suy ra: \[b^{-1} \equiv b^{M - 2} \pmod M\] Chỉ cần gọi hàm Lũy thừa nhị phân tínhpow_mod(b, M - 2, M)trongO(log M)bước. Với ví dụ(36 / 12) mod 7:a % 7 = 1, nghịch đảo của12 ≡ 5 (mod 7)là5^(7 - 2) % 7 = 3125 % 7 = 3, vậy(1 × 3) % 7 = 3. - Khi
Mkhông nguyên tố (nhưnggcd(b, M) == 1): Dùng Thuật toán Euclid mở rộng giảib × x + M × y = 1.
Trong C++ và Python, ký hiệu ^ trong công thức toán không phải phép lũy thừa trong mã nguồn: nó là XOR. Hãy dùng hàm lũy thừa nhị phân hoặc pow(a, b, M) của Python.
Ứng dụng của phép chia đồng dư là tính số tổ hợp C(n, k) = n! / (k! × (n - k)!) mod (10⁹ + 7):
Công thức dùng giai thừa và nghịch đảo dưới đây giả định 0 ≤ k ≤ n < MOD. Khi n ≥ MOD, n! chứa thừa số MOD, nên không thể nghịch đảo giai thừa theo cách này.
#include <iostream>
using namespace std;
const long long MOD = 1'000'000'007LL;
// Lũy thừa nhị phân tính a^b % m trong O(log b)
long long luy_thua_mod(long long a, long long b, long long m = MOD) {
long long res = 1;
a %= m;
while (b > 0) {
if (b & 1) res = (res * a) % m;
a = (a * a) % m;
b >>= 1;
}
return res;
}
// Nghịch đảo modulo của b theo số nguyên tố m (Định lý Fermat nhỏ: b^(m-2) % m)
long long nghich_dao_mod(long long b, long long m = MOD) {
return luy_thua_mod(b, m - 2, m);
}
// Tính tổ hợp C(n, k) % MOD = n! / (k! * (n - k)!) % MOD
long long to_hop_mod(int n, int k) {
if (k < 0 || k > n) return 0;
long long tu = 1, mau = 1;
for (int i = 1; i <= n; ++i) tu = (tu * i) % MOD;
for (int i = 1; i <= k; ++i) mau = (mau * i) % MOD;
for (int i = 1; i <= n - k; ++i) mau = (mau * i) % MOD;
return (tu * nghich_dao_mod(mau, MOD)) % MOD;
}
int main() {
// Test 1: Chia đồng dư (36 / 12) mod 7 thông qua nghịch đảo của 12 mod 7 (tức 5^-1 mod 7 = 3)
long long a_mod = 36 % 7; // 1
long long b_inv = nghich_dao_mod(12, 7); // 5^(7-2) % 7 = 3
cout << "DivMod(36/12, 7): " << (a_mod * b_inv) % 7 << "\n";
// Test 2: Tổ hợp C(10, 3) % (10^9 + 7) = 120
cout << "C(10, 3): " << to_hop_mod(10, 3) << "\n";
// Test 3: Tổ hợp lớn C(100, 50) % (10^9 + 7) = 538992043
cout << "C(100, 50): " << to_hop_mod(100, 50) << "\n";
return 0;
}
MOD = 1_000_000_007
def luy_thua_mod(a: int, b: int, m: int = MOD) -> int:
"""Lũy thừa nhị phân tính a^b % m trong O(log b)."""
return pow(a, b, m)
def nghich_dao_mod(b: int, m: int = MOD) -> int:
"""Nghịch đảo modulo của b theo số nguyên tố m: b^(m-2) % m."""
return luy_thua_mod(b, m - 2, m)
def to_hop_mod(n: int, k: int) -> int:
"""Tính tổ hợp C(n, k) % MOD = n! * (k! * (n - k)!)^(MOD - 2) % MOD."""
if k < 0 or k > n:
return 0
tu, mau = 1, 1
for i in range(1, n + 1):
tu = (tu * i) % MOD
for i in range(1, k + 1):
mau = (mau * i) % MOD
for i in range(1, n - k + 1):
mau = (mau * i) % MOD
return (tu * nghich_dao_mod(mau, MOD)) % MOD
if __name__ == "__main__":
# Test 1: Chia đồng dư (36 / 12) mod 7 thông qua nghịch đảo của 12 mod 7
a_mod = 36 % 7 # 1
b_inv = nghich_dao_mod(12, 7) # 5^(7-2) % 7 = 3
print(f"DivMod(36/12, 7): {(a_mod * b_inv) % 7}")
# Test 2: Tổ hợp C(10, 3) % (10^9 + 7) = 120
print(f"C(10, 3): {to_hop_mod(10, 3)}")
# Test 3: Tổ hợp lớn C(100, 50) % (10^9 + 7) = 538992043
print(f"C(100, 50): {to_hop_mod(100, 50)}")
Lỗi hay gặp khi tính toán với Modulo
- Chỉ lấy
% MODở lệnhcoutcuối cùng. Nếu quên% MODở từng bước gán trung gian trong vòng lặp, biếnlong longđã bị tràn số từ giữa chừng. - Viết
ans = ans + a * b % MOD;mà quên% MODcho tổng ngoài. Vì*và%có độ ưu tiên cao hơn+, biểu thức trên chỉ lấy dư phần tích rồi cộng thẳng vàoans. Hãy viết:ans = (ans + (1LL * a * b) % MOD) % MOD;. - Dùng
b^(M - 2) % MkhiMlà hợp số. Định lý Fermat nhỏ chỉ đúng khiMlà số nguyên tố; nếuMlà hợp số phải dùng Thuật toán Euclid mở rộng.
Luyện ngay trên Big-O Coding
Thực hành ngay kỹ năng tính toán đồng dư và số học trên hệ thống Big-O Coding:
Câu hỏi thường gặp
Ngoài 10^9 + 7 thì còn hằng số modulo nào hay gặp?
Phổ biến thứ hai là 998244353 (= 119 × 2²³ + 1), cũng là một số nguyên tố lớn rất thuận lợi cho phép biến đổi số học nhanh (NTT) khi nhân đa thức. Ngoài ra 10⁹ + 9 thường đi cặp với 10⁹ + 7 trong băm chuỗi kép (Double Hashing).
Trong Python số nguyên không tràn, có nên đợi đến cuối mới % MOD không?
Không! Dù int của Python tự mở rộng để chứa các số hàng vạn chữ số, phép nhân trên số khổng lồ sẽ chạy cực chậm và gây quá thời gian (TLE). Bạn vẫn phải % MOD ở mỗi bước lặp như C++.
Khi tính (a ^ b) % M với số mũ b rất lớn thì có được lấy b % M không?
Bạn được lấy a % M ở cơ số, nhưng tuyệt đối không được lấy b % M trên số mũ. Theo định lý Fermat nhỏ (khi M nguyên tố và a không chia hết cho M), số mũ b chỉ được rút gọn theo chu kỳ M - 1, tức b % (M - 1).
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í.


