Tổ Hợp, Hoán Vị & Xác Suất Cơ Bản
1. Khái niệm & bản chất của đại số tổ hợp trong lập trình thi đấu
Đại số tổ hợp (Combinatorics) là nhánh toán học nghiên cứu về việc đếm, sắp xếp và lựa chọn các phần tử trong tập hợp theo các quy tắc xác định.
Ở Level 2, bài toán tổ hợp không chỉ là tính toán công thức giải tích đơn giản mà là xử lý đa truy vấn với modulo lớn $10^9+7$:
- Hoán vị ($P_n = n!$), Chỉnh hợp ($A_n^k = \frac{n!}{(n-k)!}$), Tổ hợp ($C_n^k = \binom{n}{k} = \frac{n!}{k!(n-k)!}$).
- Tiền xử lý giai thừa & Nghịch đảo giai thừa: Tính trước $fact[i] = i! \bmod M$ và $invFact[i] = (i!)^{-1} \bmod M$ trong $\mathcal{O}(N)$ để trả lời mỗi truy vấn tính $C_n^k \bmod M$ trong $\mathcal{O}(1)$.
- Tam giác Pascal (Pascal's Triangle): Quy hoạch động tính $C_n^k = C_{n-1}^{k-1} + C_{n-1}^k$ khi modulo $M$ là hợp số.
-
Nguyên lý bù trừ (Principle of Inclusion-Exclusion — PIE): Đếm số phần tử thỏa mãn ít nhất một trong các điều kiện bằng cách xen kẽ cộng tập đơn và trừ tập giao: $$|A_1 \cup A_2 \cup \dots \cup A_n| = \sum |A_i| - \sum |A_i \cap A_j| + \sum |A_i \cap A_j \cap A_k| - \dots$$
-
Bài toán Chia kẹo của Euler (Stars and Bars): Số cách chia $N$ cái kẹo giống nhau cho $K$ đứa trẻ:
- Mỗi đứa trẻ có ít nhất 1 cái: $\binom{N - 1}{K - 1}$.
- Đứa trẻ có thể nhận 0 cái: $\binom{N + K - 1}{K - 1}$.
2. Tiền xử lý giai thừa
const int MAXN = 1000000;
const long long MOD = 1000000007;
long long fact[MAXN + 1];
long long invFact[MAXN + 1];
long long power_mod(long long a, long long b) {
long long res = 1; a %= MOD;
while (b > 0) {
if (b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b >>= 1;
}
return res;
}
void precompute_factorials() {
fact[0] = 1;
for (int i = 1; i <= MAXN; ++i) fact[i] = (fact[i - 1] * i) % MOD;
// Tính nghịch đảo giai thừa MAXN! bằng Fermat
invFact[MAXN] = power_mod(fact[MAXN], MOD - 2);
// Tính lùi: invFact[i-1] = invFact[i] * i % MOD
for (int i = MAXN - 1; i >= 0; --i) {
invFact[i] = (invFact[i + 1] * (i + 1)) % MOD;
}
}
long long nCr(int n, int r) {
if (r < 0 || r > n) return 0;
return fact[n] * invFact[r] % MOD * invFact[n - r] % MOD;
}
3. Nguyên lý bù trừ (PIE) & Đếm số nguyên tố cùng nhau
Bài toán: Đếm số lượng số trong đoạn $[1, N]$ không chia hết cho bất kỳ số nào trong tập các số nguyên tố $\{p_1, p_2, \dots, p_K\}$ ($K \le 15$).
long long count_coprime(long long n, const vector<long long> &primes) {
int k = primes.size();
long long total = 0;
for (int mask = 1; mask < (1 << k); ++mask) {
long long prod = 1;
int bits = 0;
for (int i = 0; i < k; ++i) {
if ((mask >> i) & 1) {
bits++;
prod *= primes[i];
if (prod > n) break;
}
}
long long cnt = n / prod;
if (bits % 2 == 1) total += cnt; // Số lẻ tập: Cộng vào
else total -= cnt; // Số chẵn tập: Trừ ra
}
return n - total; // Số lượng không chia hết cho bất kỳ số nào
}
4. Ranh giới áp dụng
| Tình Huống | Điều Kiện Modulo $M$ | Kỹ Thuật Tối Ưu |
|---|---|---|
| $N \le 10^6, Q \le 10^5$ | $M$ là số nguyên tố ($10^9+7$) | Tiền xử lý fact và invFact $\implies \mathcal{O}(1)$ mỗi truy vấn |
| $N \le 2000, Q \le 10^5$ | $M$ là hợp số bất kỳ | Tam giác Pascal DP $\mathcal{O}(N^2)$ |
| $N \le 10^{18}, K \le 10^6$ | $M$ nguyên tố | Tính trực tiếp $C_n^k = \frac{n(n-1)\dots(n-k+1)}{k!} \bmod M$ |