Tổ hợp và đếm cơ bản
Rất nhiều bài lập trình thi đấu hỏi "có bao nhiêu cách...". Duyệt trâu từng cấu hình tốn thời gian tỉ lệ với chính số cách (thường là , hay lớn hơn), trong khi một công thức tổ hợp cho đáp án trong sau khi tiền xử lý . Trang này đi từ hai quy tắc đếm cơ bản đến hoán vị, chỉnh hợp, tổ hợp, cách tính theo modulo nguyên tố và modulo hợp số, bài toán chia kẹo và số Catalan.
Ý tưởng / Trực giác
Hai quy tắc nền tảng
- Quy tắc cộng: nếu các cấu hình chia được thành những nhóm rời nhau (không cấu hình nào thuộc hai nhóm), số cấu hình bằng tổng số cấu hình của từng nhóm. Đây là cơ sở của mọi lời giải "xét từng trường hợp".
- Quy tắc nhân: nếu một cấu hình được dựng qua nhiều bước liên tiếp, và số lựa chọn ở mỗi bước không phụ thuộc vào lựa chọn cụ thể ở các bước trước, thì số cấu hình bằng tích số lựa chọn. Ví dụ: xâu độ dài trên bảng chữ cái ký tự có cách.
Hai điều kiện in đậm là chỗ hay sai nhất. Cộng hai nhóm chồng lên nhau thì đếm trùng (khi đó cần bao hàm loại trừ); nhân khi số lựa chọn thay đổi theo bước trước thì sai hoàn toàn.
Một mẹo đi kèm là đếm phần bù: đếm cấu hình "tốt" khó thì đếm tất cả trừ đi cấu hình "xấu".
Hoán vị, chỉnh hợp, tổ hợp
Chọn và xếp phần tử từ phần tử phân biệt:
| Đối tượng | Có thứ tự? | Công thức | Lập luận |
|---|---|---|---|
| Hoán vị phần tử | có | vị trí 1 có lựa chọn, vị trí 2 còn , ... | |
| Chỉnh hợp chập | có | quy tắc nhân, dừng sau vị trí | |
| Tổ hợp chập | không | mỗi tập phần tử ứng với đúng chỉnh hợp | |
| Hoán vị lặp (đa tập) | có | các phần tử giống nhau đổi chỗ cho nhau không tạo cách mới |
Dòng thứ ba là mẫu lập luận quan trọng nhất của cả trang: đếm có thứ tự rồi chia cho số lần mỗi cấu hình bị đếm lặp. Nó chỉ đúng khi mọi cấu hình bị đếm lặp cùng một số lần.
Tam giác Pascal: tổ hợp chỉ bằng phép cộng
Xét phần tử cuối cùng: một tập phần tử của hoặc không chứa (chọn đủ từ phần tử còn lại) hoặc chứa (chọn thêm ). Theo quy tắc cộng:
Công thức này không có phép chia, nên đúng với mọi modulo, kể cả modulo hợp số hay khi đếm chính xác bằng số lớn. Cái giá là phép cộng, chỉ dùng được khi cỡ vài nghìn. Bộ nhớ cũng cần để ý: bảng đầy đủ long long C[2001][2001] đã chiếm khoảng MB. Nếu chỉ cần một hàng thì giữ một mảng một chiều và cập nhật từ phải sang trái, bộ nhớ còn .
Vì sao cần nghịch đảo, và vì sao là Fermat
Khi tới mà cần nhiều truy vấn, ta dùng . Trong số học đồng dư không có phép chia, chỉ có nhân với nghịch đảo: là số thỏa .
Với nguyên tố và , định lý Fermat nhỏ cho , nên
tính bằng lũy thừa nhanh . Nghịch đảo tồn tại khi và chỉ khi . Mọi thừa số trong đều khi , nên không chia hết cho và luôn nghịch đảo được. Các modulo quen thuộc và được chọn chính vì chúng nguyên tố và lớn hơn mọi trong đề.
Mẹo tiết kiệm: chỉ cần một lần lũy thừa. Tính inv_fact[N] , rồi đi lùi: .
Chia kẹo (stars and bars)
Đếm số nghiệm nguyên không âm của (chia viên kẹo giống nhau cho em). Xếp ngôi sao * và vách | thành một hàng: vách chia hàng sao thành nhóm, số sao nhóm thứ chính là . Đây là song ánh, nên đáp án là số cách chọn vị trí cho vách trong ô:
Nếu yêu cầu thì phát trước mỗi em một viên, còn viên chia tự do: . Tổng quát, thì trừ trước . Ràng buộc chặn trên thì cần bao hàm loại trừ.
Một biến thể hay gặp: số dãy không giảm độ dài với giá trị trong bằng (đặt là số lần giá trị xuất hiện).
Số Catalan và nguyên lý phản xạ
Số dãy ngoặc đúng gồm cặp ngoặc là số Catalan
Coi ( là bước lên , ) là bước xuống . Dãy ngoặc đúng là đường đi bước từ độ cao về không bao giờ xuống dưới . Tổng số đường từ về (bất kể điều kiện) là . Đường xấu là đường chạm độ cao . Lấy điểm đầu tiên nó chạm , lật đối xứng toàn bộ phần phía trước qua đường : đường mới xuất phát từ và kết thúc ở , tức có bước lên và bước xuống. Phép lật này là song ánh giữa đường xấu và mọi đường từ tới , nên số đường xấu là . Trừ đi được công thức trên.
Nguyên lý phản xạ dùng được cả khi đã biết trước một tiền tố: nếu còn ký tự phải điền, độ cao hiện tại là , cần dấu ) thì đáp án là .
Các bài quy về Catalan (đáp án đều là ):
- số dãy ngoặc đúng cặp; số đường đi trên lưới từ góc dưới trái lên góc trên phải, chỉ đi phải/lên, không vượt lên trên đường chéo;
- số cây nhị phân có nút; số cách đặt ngoặc đầy đủ một tích thừa số (biểu thức toán hạng có cách);
- số cách chia đa giác lồi đỉnh thành tam giác bằng các đường chéo không cắt nhau;
- số hoán vị phần tử tránh một mẫu độ dài 3 cố định bất kỳ (ví dụ mẫu 321: không có 3 phần tử giảm dần; hay mẫu 231: sắp xếp được bằng một ngăn xếp);
- công thức truy hồi , (tách theo vị trí dấu
)khớp với dấu(đầu tiên), không có phép chia nên dùng được với mọi modulo khi nhỏ.
Dấu hiệu nhận ra Catalan: tính tay đáp án cho ra .
Khi modulo là hợp số
Với modulo như , hay cho trong input, cách giai thừa + Fermat sai: không phải nghịch đảo, và tệ hơn, có thể chia hết cho một ước nguyên tố của nên không có nghịch đảo nào. Ví dụ với : . Có hai hướng xử lý:
- Phân tích đáp án ra thừa số nguyên tố (công thức Legendre). Số mũ của nguyên tố trong là (có số chia hết cho , trong đó số góp thêm một thừa số nữa, ...). Vậy với . Chỉ còn phép nhân, không còn phép chia. Hợp với một vài giá trị cần tính, tới cỡ (cần sàng nguyên tố).
- Tách các ước nguyên tố của modulo ra riêng. Phân tích (với thì ). Viết mỗi số thành (phần gồm các ) (phần nguyên tố cùng nhau với ). Phần sau nghịch đảo được bằng Euclid mở rộng; phần trước chỉ lưu số mũ rồi cộng trừ. Hợp với nhiều truy vấn.
Với modulo lớn cỡ , tích hai số dư đã vượt nên phép a * b % m bằng long long bị tràn. Hai đoạn code ở mục Cài đặt nhân qua __int128 nên phép nhân đúng với mọi , kể cả . Riêng hướng 2 còn phải phân tích : code mẫu chia thử tới , chỉ nhanh khi tới khoảng (cỡ bước). Với lớn hơn mà có ước nguyên tố lớn, vòng chia thử có thể tới cỡ bước; khi đó thay bước phân tích bằng Pollard rho.
Khi nguyên tố nhưng không lớn hơn (tức , ví dụ ), chứa thừa số nên và công thức giai thừa cũng hỏng: dùng định lý Lucas.
Khi nào phải đếm chính xác
Đề không cho modulo nghĩa là cần giá trị thật. Các mốc nên thuộc:
| Đại lượng | Lớn nhất vừa long long () |
Lớn nhất vừa __int128 () |
|---|---|---|
| (mọi ) | ||
| Catalan |
Vượt các mốc này thì dùng Pascal (chỉ cộng) trên số lớn, hoặc Python với số nguyên không giới hạn.
Ví dụ chạy tay
Tam giác Pascal tới hàng 5
n\k 0 1 2 3 4 5
0 1
1 1 1
2 1 2 1
3 1 3 3 1
4 1 4 [6] 4 1
5 1 5 10 10 5 1
^
C(5,2) = C(4,1) + C(4,2) = 4 + [6] = 10
Mỗi ô bằng ô ngay trên cộng ô trên bên trái. Lấy modulo sau mỗi phép cộng là đủ, với bất kỳ modulo nào.
Giai thừa và nghịch đảo giai thừa với
Tính (đáp án thật ).
i : 0 1 2 3 4 5
fact[i] : 1 1 2 6 3 1 (24 = 3 mod 7, 120 = 1 mod 7)
inv_fact[5] = fact[5]^(7-2) = 1^5 = 1
đi lùi inv_fact[i-1] = inv_fact[i] * i :
inv_fact : 1 1 4 6 5 1
^ ^
kiểm tra: 2*4 = 8 = 1, 6*6 = 36 = 1 (mod 7)
C(5,2) = fact[5] * inv_fact[2] * inv_fact[3] = 1 * 4 * 6 = 24 = 3 (mod 7) đúng
Chia kẹo: 5 viên cho 3 em
* * | * | * * -> (2, 1, 2)
| * * * * * | -> (0, 5, 0)
* * * * * | | -> (5, 0, 0)
7 ô, chọn 2 ô làm vách: C(7, 2) = 21 cách
Phản xạ với cặp ngoặc
Đường ())(() là đường xấu: nó xuống tới ở bước 3. Lật phần từ đầu tới điểm chạm đầu tiên qua đường (đổi ( thành ) và ngược lại, điểm xuất phát thành ), giữ nguyên phần sau:
bước : 0 1 2 3 4 5 6
đường xấu : ( ) ) ( ( )
độ cao : 0 1 0 -1 0 1 0
^ chạm -1 lần đầu ở bước 3
đường lật : ) ( ( ( ( )
độ cao : -2 -3 -2 -1 0 1 0
^ xuất phát từ -2 phần sau bước 3 giữ nguyên
Ngược lại, mọi đường từ về đều phải cắt , lật lại phần trước điểm cắt đầu tiên sẽ ra đúng một đường xấu. Đường sau khi lật đi từ lên trong bước: bước lên, bước xuống. Số đường như vậy là , bằng số đường xấu. Vậy , khớp năm dãy ((())), (()()), (())(), ()(()), ()()().
Legendre:
nên không nghịch đảo được. Tính số mũ từng nguyên tố:
p | v_p(10!) | v_p(4!) | v_p(6!) | e_p
---+-----------------+-----------+-----------+-----
2 | 5 + 2 + 1 = 8 | 2 + 1 = 3 | 3 + 1 = 4 | 1
3 | 3 + 1 = 4 | 1 | 2 | 1
5 | 2 | 0 | 1 | 1
7 | 1 | 0 | 0 | 1
C(10,4) = 2^1 * 3^1 * 5^1 * 7^1 = 210 = 6 (mod 12)
Cài đặt
Tam giác Pascal (modulo bất kỳ, nhỏ)
Chỉ cần một hàng nên dùng mảng một chiều: đi từ phải sang trái để C[j - 1] vẫn là giá trị của hàng trước. Nếu cần nhiều truy vấn khác nhau thì lưu cả bảng, nhưng nhớ bảng long long tốn khoảng MB (dùng int khi để giảm còn một nửa).
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k; long long M;
cin >> n >> k >> M; // any modulus 1 <= M < 2^62
if (k < 0 || k > n) { cout << 0 << "\n"; return 0; }
vector<long long> C(n + 1, 0); // one row of Pascal's triangle, O(n) memory
C[0] = 1 % M; // row 0: C(0,0) = 1
for (int i = 1; i <= n; i++)
for (int j = i; j >= 1; j--) // right to left: C[j-1] still holds row i-1
C[j] = (C[j] + C[j - 1]) % M; // Pascal: only additions, no division
cout << C[k] << "\n";
}
Giai thừa + nghịch đảo giai thừa (modulo nguyên tố)
Khung chuẩn, dùng cho tổ hợp, chia kẹo và Catalan. Mỗi truy vấn .
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1'000'000'007; // prime
const int MAXN = 2'000'000; // must cover the largest n used
long long fact[MAXN + 1], inv_fact[MAXN + 1];
long long power(long long a, long long e) { // a^e mod MOD, O(log e)
long long r = 1; a %= MOD;
while (e > 0) {
if (e & 1) r = r * a % MOD;
a = a * a % MOD;
e >>= 1;
}
return r;
}
void precompute() {
fact[0] = 1;
for (int i = 1; i <= MAXN; i++) fact[i] = fact[i - 1] * i % MOD;
inv_fact[MAXN] = power(fact[MAXN], MOD - 2); // Fermat: a^(p-2) = a^(-1)
for (int i = MAXN; i > 0; i--) inv_fact[i - 1] = inv_fact[i] * i % MOD;
}
long long C(long long n, long long k) {
if (k < 0 || k > n) return 0; // out of range: 0 ways
return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}
long long stars_bars(long long n, long long k) { // n candies, k kids, >= 0 each
if (k == 0) return n == 0 ? 1 : 0; // no kids: only n = 0 works
return C(n + k - 1, k - 1);
}
long long catalan(int n) { // C(2n,n) - C(2n,n+1)
return (C(2 * n, n) - C(2 * n, n + 1) + MOD) % MOD;
}
// Input: q, then q lines, each one of
// C n k -> C(n, k)
// S n k -> n candies, k kids (>= 0 each)
// K n -> Catalan number C_n (only one number)
int main() {
precompute();
int q; cin >> q;
while (q--) {
char type; long long a, b = 0;
cin >> type >> a;
if (type != 'K') cin >> b; // 'K' has no second number
if (type == 'C') cout << C(a, b) << "\n"; // binomial
else if (type == 'S') cout << stars_bars(a, b) << "\n"; // stars and bars
else cout << catalan(a) << "\n"; // 'K': Catalan number
}
}
Lưu ý MAXN phải phủ đối số lớn nhất thực sự dùng: chia kẹo cần tới , Catalan cần tới . Hàm stars_bars tách riêng trường hợp : công thức luôn trả , trong khi theo quy ước chia viên cho em có đúng cách.
Dãy ngoặc khi biết trước tiền tố (phản xạ)
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1'000'000'007;
vector<long long> fact, inv_fact;
long long power(long long a, long long e) {
long long r = 1; a %= MOD;
for (; e > 0; e >>= 1, a = a * a % MOD)
if (e & 1) r = r * a % MOD;
return r;
}
long long C(long long n, long long k) {
if (k < 0 || k > n) return 0;
return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}
int main() {
int n; string s;
cin >> n >> s;
fact.assign(n + 1, 1); inv_fact.assign(n + 1, 1);
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
inv_fact[n] = power(fact[n], MOD - 2);
for (int i = n; i > 0; i--) inv_fact[i - 1] = inv_fact[i] * i % MOD;
int bal = 0;
for (char c : s) {
bal += (c == '(') ? 1 : -1;
if (bal < 0) { cout << 0 << "\n"; return 0; } // prefix already invalid
}
int r = n - (int)s.size(); // characters still to place
if ((r + bal) % 2 != 0 || bal > r) { cout << 0 << "\n"; return 0; }
int d = (r + bal) / 2; // number of ')' still needed
// all paths from height bal to 0, minus the bad ones (reflection)
cout << (C(r, d) - C(r, d + 1) + MOD) % MOD << "\n";
}
theo modulo bất kỳ bằng công thức Legendre
Chương trình đầy đủ; main in như ví dụ chạy tay. Phép nhân đi qua __int128 nên đúng với mọi , kể cả . Cách này không cần phân tích , nên lớn cũng không làm chậm.
#include <bits/stdc++.h>
using namespace std;
long long mul_mod(long long a, long long b, long long m) { // (a*b) % m without overflow
return (long long)((__int128)a * b % m);
}
long long power_mod(long long a, long long e, long long m) {
long long r = 1 % m; a %= m;
for (; e > 0; e >>= 1, a = mul_mod(a, a, m))
if (e & 1) r = mul_mod(r, a, m);
return r;
}
// exponent of prime p in x! (Legendre)
long long legendre(long long x, long long p) {
long long e = 0;
while (x > 0) { x /= p; e += x; }
return e;
}
// C(n,k) mod M for any 1 <= M < 2^62, n up to ~1e7
long long binom_any_mod(int n, int k, long long M) {
if (k < 0 || k > n) return 0;
vector<bool> composite(n + 1, false); // sieve primes up to n
long long ans = 1 % M;
for (long long p = 2; p <= n; p++) {
if (composite[p]) continue;
for (long long q = p * p; q <= n; q += p) composite[q] = true;
long long e = legendre(n, p) - legendre(k, p) - legendre(n - k, p);
if (e > 0) ans = mul_mod(ans, power_mod(p, e, M), M);
}
return ans;
}
int main() {
cout << binom_any_mod(10, 4, 12) << "\n"; // 6
cout << binom_any_mod(17, 3, 1LL << 32) << "\n"; // 680
}
Cùng cách này tính được mọi biểu thức dạng tích và thương các giai thừa, ví dụ Catalan : chỉ cần đổi công thức .
Nhiều truy vấn với modulo hợp số: tách ước nguyên tố của modulo
Chương trình đầy đủ, cũng nhân qua __int128 nên mọi phép nhân đúng với . Tuy vậy hàm dựng phân tích bằng chia thử tới , tốn : nhanh khi tới khoảng (cỡ bước), nhưng với gần có ước nguyên tố lớn thì vòng này tới cỡ bước, quá chậm. Khi đó thay đoạn chia thử bằng Pollard rho; phần còn lại giữ nguyên.
#include <bits/stdc++.h>
using namespace std;
// Many queries C(n,k) mod M (M not necessarily prime).
// Multiplication is overflow-safe for M < 2^62, but the trial-division
// factoring below is fast only for M up to ~1e12 (use Pollard rho beyond).
// Every i = (part made of M's primes) * (part coprime to M).
// The coprime part is invertible mod M; the other part is kept as exponents.
struct BinomAnyMod {
long long M;
vector<long long> ps; // distinct primes of M
vector<long long> fr; // fr[i] = product of coprime parts of 1..i, mod M
vector<vector<int>> ex; // ex[j][i] = exponent of ps[j] in i!
long long mul(long long a, long long b) const { // (a*b) % M via __int128, no overflow
return (long long)((__int128)a * b % M);
}
static long long ext_gcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) { x = 1; y = 0; return a; }
long long x1, y1, g = ext_gcd(b, a % b, x1, y1);
x = y1; y = x1 - (a / b) * y1;
return g;
}
long long inv(long long a) const { // a must be coprime to M
long long x, y;
ext_gcd(a, M, x, y);
return (x % M + M) % M;
}
long long power(long long a, long long e) const {
long long r = 1 % M; a %= M;
for (; e > 0; e >>= 1, a = mul(a, a))
if (e & 1) r = mul(r, a);
return r;
}
BinomAnyMod(int maxn, long long mod) : M(mod) {
long long t = M; // factor M by trial division, O(sqrt M)
for (long long p = 2; p * p <= t; p++)
if (t % p == 0) { ps.push_back(p); while (t % p == 0) t /= p; }
if (t > 1) ps.push_back(t);
fr.assign(maxn + 1, 1 % M);
ex.assign(ps.size(), vector<int>(maxn + 1, 0));
for (int i = 1; i <= maxn; i++) {
long long v = i;
for (size_t j = 0; j < ps.size(); j++) {
int c = 0;
while (v % ps[j] == 0) { v /= ps[j]; c++; }
ex[j][i] = ex[j][i - 1] + c;
}
fr[i] = mul(fr[i - 1], v % M);
}
}
long long C(int n, int k) const {
if (k < 0 || k > n) return 0;
long long r = mul(mul(fr[n], inv(fr[k])), inv(fr[n - k]));
for (size_t j = 0; j < ps.size(); j++)
r = mul(r, power(ps[j], ex[j][n] - ex[j][k] - ex[j][n - k]));
return r;
}
};
int main() {
BinomAnyMod small(100, 12);
cout << small.C(10, 4) << "\n"; // 6, matches the hand-worked example
BinomAnyMod big(100, 1LL << 32);
cout << big.C(17, 3) << "\n"; // 680
}
Python: đếm chính xác và bản modulo
from math import comb
# exact values: Python ints never overflow
print(comb(100, 50)) # 100891344545564193334812497256
print(comb(2 * 40, 40) // 41) # Catalan C_40, already beyond long long
MOD = 10**9 + 7
N = 10**6
fact = [1] * (N + 1)
for i in range(1, N + 1):
fact[i] = fact[i - 1] * i % MOD
inv_fact = [1] * (N + 1)
inv_fact[N] = pow(fact[N], MOD - 2, MOD) # Fermat
for i in range(N, 0, -1):
inv_fact[i - 1] = inv_fact[i] * i % MOD
def C(n, k):
return 0 if k < 0 or k > n else fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD
math.comb tiện cho đếm chính xác với vài nghìn, nhưng không dùng nó rồi mới % MOD khi tới : số trung gian có hàng trăm nghìn chữ số, rất chậm.
Độ phức tạp
| Cách tính | Tiền xử lý | Mỗi truy vấn | Bộ nhớ | Vì sao |
|---|---|---|---|---|
| Tam giác Pascal | cả bảng, một hàng | mỗi ô là một phép cộng; có ô | ||
| Giai thừa + nghịch đảo (mod nguyên tố) | hai vòng lặp tuyến tính và một lần lũy thừa nhanh | |||
| Catalan, phản xạ, chia kẹo | như trên | chỉ là hai hoặc ba lần gọi C |
||
| Legendre (mod bất kỳ) | không | sàng tới ; mỗi nguyên tố tốn cho Legendre và cho lũy thừa | ||
| Tách ước nguyên tố của modulo | ước nguyên tố (khi ); mỗi truy vấn hai nghịch đảo Euclid mở rộng và lần lũy thừa. Số hạng là chia thử: ổn khi tới khoảng , lớn hơn thì dùng Pollard rho (khoảng ) |
⚠️ Lỗi thường gặp
- Tràn số khi nhân ba thừa số.
fact[n] * inv_fact[k] * inv_fact[n-k] % MODtrànlong longvì tích hai số cỡ đã gần . Phải% MODsau mỗi phép nhân, như trong code mẫu. - Modulo lớn hơn khoảng . Khi (ví dụ ), ngay cả
a * b % mvới cũng trànlong long. Nhân qua__int128:(__int128)a * b % m. - Dùng Fermat khi modulo không nguyên tố. Với , hay đọc từ input,
power(x, m - 2)cho kết quả sai mà không báo lỗi. Đọc kỹ modulo trong đề; nếu hợp số dùng Legendre, tách ước nguyên tố của modulo, hoặc Pascal. - Modulo nguyên tố nhưng không lớn hơn . Khi thì
fact[n] % p == 0, mọi tính ra đều . Dùng Lucas. - Mảng giai thừa không đủ dài. Chia kẹo gọi và Catalan gọi : kích thước cần là hay , không phải . Thiếu thì truy cập ngoài mảng, kết quả rác.
- Hiệu hai tổ hợp bị âm. tính theo modulo có thể ra số âm; luôn
(a - b + MOD) % MOD. - Quên trường hợp hay . Phản xạ có thể gọi
C(r, d + 1)với ; chia kẹo với có thể gọiC(n - 1, k - 1)với . HàmCphải trả ở các trường hợp này, nếu không sẽ đọc chỉ số âm. - Chia cho số lần lặp không đều. "Đếm có thứ tự rồi chia" chỉ đúng khi mọi cấu hình bị đếm lặp như nhau. Ví dụ đếm vòng tròn xếp hạt có đối xứng (xoay, lật) thì số lần lặp khác nhau giữa các cấu hình: phải dùng Burnside.
- Cộng các trường hợp chồng lên nhau. "Có ít nhất một chữ số 7" không phải là cộng "vị trí 1 là 7" + "vị trí 2 là 7" + ...; các nhóm này giao nhau. Đếm phần bù () hoặc bao hàm loại trừ.
- Đếm chính xác nhưng dùng
long long. , , đã vượt . Ngay cả khi đáp án vừa, công thức lặpr = r * (n - i) / (i + 1)vẫn có thể tràn ở bước nhân trung gian; dùng__int128cho phép nhân hoặc Pascal. - Mảng Pascal hai chiều quá lớn.
long long C[2001][2001]đã khoảng MB,C[5001][5001]khoảng MB. Chỉ cần một hàng thì dùng mảng một chiều.
Biến thể / Mở rộng
- Định lý Lucas cho khi tới và nhỏ; kết hợp CRT cho modulo hợp số không bình phương: Lucas Theorem.
- Bao hàm loại trừ: chia kẹo có chặn trên, đếm song ánh không điểm cố định (derangement), đếm theo tập con: Bao hàm loại trừ.
- Đếm dưới tác động đối xứng (vòng cổ, tô màu khối lập phương): Burnside và Pólya.
- Đếm có điều kiện phức tạp thường chuyển sang quy hoạch động đếm; công thức tổ hợp lúc đó đóng vai trò chuyển trạng thái nhanh, ví dụ QHĐ chữ số.
- Thứ hạng của hoán vị / tổ hợp (hoán vị thứ , tổ hợp đứng thứ mấy theo từ điển) dùng giai thừa và để "nhảy" qua cả khối: Hạng hoán vị.
- Phân tích modulo lớn (tới cỡ ) để tách ước nguyên tố: Miller-Rabin và Pollard rho.
- Nền tảng số học đồng dư (lũy thừa nhanh, nghịch đảo, CRT): Toán học.
Bài tập luyện
Luyện kỹ thuật
- Hệ số nhị thức (binocoef) (Nghiệp dư) truy vấn với : bài luyện trực tiếp khung giai thừa + nghịch đảo giai thừa.
- Phân phát táo (distapples) (Nghiệp dư) chia quả táo giống nhau cho em, mỗi em được quả trở lên: đúng bài chia kẹo, chú ý kích thước mảng giai thừa.
- Dãy ngoặc I (bracketseq1) (Nghiệp dư) đếm dãy ngoặc đúng độ dài : số Catalan, nhớ xử lý lẻ.
- Dãy ngoặc II (bracketseq2) (Trung cấp) đã biết một tiền tố: áp dụng nguyên lý phản xạ từ độ cao hiện tại thay vì từ .
- Dãy điểm của Dima (twoseq) (Nâng cao) đáp án là tích các giai thừa chia cho lũy thừa của , lấy theo modulo tùy ý (có thể chẵn): không nghịch đảo được, phải xử lý thừa số riêng.
Đề VOI
- Kế hoạch cải tổ (voi15_reform) (Nâng cao) VOI 2015: phân loại cạnh bị bỏ theo cầu / không cầu, rồi đếm từng loại bằng quy tắc cộng và quy tắc nhân.
- Phép toán OR (voi21_or) (Nâng cao) VOI 2021: số bộ chỉ số chọn trong chỉ số "được phép" là ; cần bảng giai thừa, sau đó kết hợp với bao hàm loại trừ trên tập con.
- Hàng cây (voi11_tline) (Kỳ cựu) VOI 2011: câu hai đếm hoán vị không có 3 phần tử giảm dần; liệt kê đáp án với số cây nhỏ để nhận ra dãy quen thuộc, và để ý modulo không nguyên tố.
- Sản xuất đồ chơi (voi13_organ) (Kỳ cựu) VOI 2013: số cách xếp một lô chỉ phụ thuộc số ống, lập bảng truy hồi kiểu Pascal bằng cách chèn ống cao nhất; đề không lấy modulo nên cần số lớn.
- Tất niên (voi26_tet) (Kỳ cựu) VOI 2026: độ đa dạng của một đoạn: đếm các cặp vị trí có giá trị khác nhau, mỗi cặp cho đúng một dãy mới; phần khó còn lại là duy trì đại lượng này khi đoạn thay đổi.
- Mã hóa dãy số (voi25_encode) (Chuyên gia) VOI 2025: sau khi biết có bao nhiêu vị trí buộc phải tăng ngặt, bài toán còn lại là đếm dãy không giảm với giá trị bị chặn, tức chia kẹo theo modulo .
Bình luận