Banyak soal DSA — menghitung banyak cara menyusun tim, koefisien binomial pada ekspansi (a+b)ⁿ, hingga koefisien pada algoritma DP — memerlukan perhitungan permutasi P(n, k) dan kombinasi C(n, k). Tanpa teknik modular, factorial n! meluap dengan cepat (21! sudah lebih dari 2⁶³). Pada Chapter ini kita menurunkan rumus keduanya, lalu membangun struktur precompute factorial + inverse factorial untuk menjawab banyak query C(n, k) mod p dalam O(1) per query, dengan p = 10⁹+7.
Definisi Permutasi dan Kombinasi #
Aturan #
Diberikan himpunan n elemen berbeda:
- Permutasi
P(n, k) = n! / (n - k)!: banyak cara memilihkelemen berurutan darinelemen. Urutan penting. - Kombinasi
C(n, k) = n! / (k! · (n - k)!): banyak cara memilihkelemen tanpa urutan darinelemen. Urutan tidak penting. - Hubungan:
P(n, k) = C(n, k) · k!. Kombinasi adalah permutasi yang membagik!(banyak urutan internal yang setara).
Contoh: dari {1, 2, 3, 4} pilih 2:
- Permutasi:
(1,2), (1,3), (1,4), (2,1), (2,3), (2,4), (3,1), (3,2), (3,4), (4,1), (4,2), (4,3)→ 12 =4!/2!. - Kombinasi:
{1,2}, {1,3}, {1,4}, {2,3}, {2,4}, {3,4}→ 6 =4!/(2!·2!).
Mekanisme: Mengapa Rumus Berlaku #
P(n, k): tahap 1 ada n pilihan, tahap 2 ada n-1 (satu sudah dipakai), …, tahap k ada n-k+1. Total via product rule: n · (n-1) · ... · (n-k+1) = n! / (n-k)!.
C(n, k): dari P(n, k) cara terurut, bagi k! karena k! urutan internal yang berbeda menghasilkan kombinasi yang sama. Maka C(n, k) = P(n, k) / k! = n! / (k! · (n-k)!).
Identitas Penting #
Aturan #
Beberapa identitas yang sering muncul:
Pascal identity : C(n, k) = C(n-1, k-1) + C(n-1, k)
Symmetry : C(n, k) = C(n, n-k)
Summation : Σ_{k=0}^{n} C(n, k) = 2^n (baris ke-n segitiga Pascal)
Vandermonde : Σ_{i} C(m, i) · C(n, k-i) = C(m+n, k)Mekanisme: Pembuktian Pascal Secara Kombinatorial #
C(n, k) menghitung banyak subset ukuran k dari n elemen. Pisahkan menjadi: (a) subset yang memuat elemen ke-n, dan (b) subset yang tidak memuat elemen ke-n. Jenis (a): pilih k-1 dari n-1 sisanya → C(n-1, k-1). Jenis (b): pilih k dari n-1 sisanya → C(n-1, k). Total C(n-1, k-1) + C(n-1, k).
Komputasi Modular: Precompute Faktorial #
Aturan #
Untuk menghitung C(n, k) mod p (dengan p prima seperti 10⁹+7):
- Precompute
fact[i] = i! mod puntuki = 0..N(sekali,O(N)). - Precompute
invfact[i] = (i!)⁻¹ mod puntuki = 0..N(O(N) + O(log p)via Fermat’s little theorem). - Query
C(n, k) = fact[n] · invfact[k] · invfact[n-k] mod p—O(1)per query.
Mekanisme: Fermat untuk Invers Faktorial #
Untuk prima p, Fermat’s little theorem menyatakan a^(p-1) ≡ 1 (mod p) bila gcd(a, p) = 1. Maka a^(-1) ≡ a^(p-2) (mod p). Invers dari N! adalah (N!)^(p-2) mod p, dihitung dengan fast exponentiation dalam O(log p).
Setelah invfact[N] diketahui, kita turun mundur: invfact[i-1] = invfact[i] · i mod p (karena invfact[i] · i! = 1, maka invfact[i-1] = invfact[i] · i). Ini menghemat O(N log p) menjadi O(N + log p).
Kode: Precompute + Query #
#include <iostream>
#include <vector>
const long long MOD = 1'000'000'007;
// Fast exponentiation modulo (lihat chapter Fast Exponentiation)
long long power(long long b, long long k, long long m) {
long long r = 1; b %= m;
while (k > 0) {
if (k & 1) r = (r * b) % m;
b = (b * b) % m;
k >>= 1;
}
return r;
}
struct Factorials {
int n;
std::vector<long long> fact, invfact;
explicit Factorials(int n_) : n(n_), fact(n_ + 1), invfact(n_ + 1) {
fact[0] = 1;
for (int i = 1; i <= n; ++i) fact[i] = fact[i - 1] * i % MOD;
invfact[n] = power(fact[n], MOD - 2, MOD);
for (int i = n - 1; i >= 0; --i)
invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}
// C(n, k) = n! / (k! (n-k)!)
long long C(int n_, int k) const {
if (k < 0 || k > n_) return 0;
return fact[n_] * invfact[k] % MOD * invfact[n_ - k] % MOD;
}
// P(n, k) = n! / (n-k)!
long long P(int n_, int k) const {
if (k < 0 || k > n_) return 0;
return fact[n_] * invfact[n_ - k] % MOD;
}
};
// Pascal identity: C(n, k) = C(n-1, k-1) + C(n-1, k)
long long pascal_C(int n, int k) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1;
return pascal_C(n - 1, k - 1) + pascal_C(n - 1, k);
}
int main() {
Factorials F(20);
// P(n, k) dan C(n, k) untuk n=5..7
std::cout << "Permutasi P(n, k):\n";
for (int n = 5; n <= 7; ++n) {
for (int k = 0; k <= n; ++k)
std::cout << "P(" << n << "," << k << ")=" << F.P(n, k) << ' ';
std::cout << '\n';
}
std::cout << "\nKombinasi C(n, k) (segitiga Pascal):\n";
for (int n = 0; n <= 6; ++n) {
for (int k = 0; k <= n; ++k)
std::cout << F.C(n, k) << ' ';
std::cout << '\n';
}
// Verifikasi Pascal identity dengan rekursi (naif, tanpa mod)
std::cout << "\nVerifikasi Pascal C(6,3) rekursif: " << pascal_C(6, 3) << '\n';
// Mod test: C(10^6, 5) mod 1e9+7
Factorials Fbig(1'000'000);
std::cout << "C(10^6, 5) mod 1e9+7 = " << Fbig.C(1'000'000, 5) << '\n';
// Manual check: 10^6 * (10^6-1) * ... * (10^6-4) / 120
long long num = 1;
for (int i = 0; i < 5; ++i) num = num * (1'000'000 - i) % MOD;
long long denom_inv = power(120, MOD - 2, MOD);
std::cout << "Manual check = " << num * denom_inv % MOD << '\n';
return 0;
}Output yang Dihasilkan:
Permutasi P(n, k):
P(5,0)=1 P(5,1)=5 P(5,2)=20 P(5,3)=60 P(5,4)=120 P(5,5)=120
P(6,0)=1 P(6,1)=6 P(6,2)=30 P(6,3)=120 P(6,4)=360 P(6,5)=720 P(6,6)=720
P(7,0)=1 P(7,1)=7 P(7,2)=42 P(7,3)=210 P(7,4)=840 P(7,5)=2520 P(7,6)=5040 P(7,7)=5040
Kombinasi C(n, k) (segitiga Pascal):
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
Verifikasi Pascal C(6,3) rekursif: 20
C(10^6, 5) mod 1e9+7 = 287783408
Manual check = 287783408- Baris
fact[0] = 1;: base case —0! = 1menurut konvensi (produk kosong). - Baris
fact[i] = fact[i - 1] * i % MOD;: precompute iteratifi!. Modulo setiap iterasi. - Baris
invfact[n] = power(fact[n], MOD - 2, MOD);: inversn!via Fermat:a^(p-2) ≡ a^(-1) (mod p). Lihat Chapter Modular Inverse . - Baris
invfact[i] = invfact[i + 1] * (i + 1) % MOD;: turun mundur.(i+1)!⁻¹ · (i+1) = i!⁻¹. Lebih cepat dari menghitungi!⁻¹satu per satu viapower. - Baris
return fact[n_] * invfact[k] % MOD * invfact[n_ - k] % MOD;:C(n, k) = n! / (k! · (n-k)!) = n! · (k!)⁻¹ · ((n-k)!)⁻¹. Dua kalimoduntuk mencegah overflow intermediate (long longaman hingga(10⁹)² ≈ 10¹⁸). - Hasil
P(5, 3) = 60:5!/2! = 120/2 = 60. Sesuai. - Hasil
P(7, 5) = 2520:7!/2! = 5040/2 = 2520. Sesuai. - Segitiga Pascal: baris ke-
nberisiC(n, 0), C(n, 1), ..., C(n, n). Baris ke-6:1 6 15 20 15 6 1— simetris sesuai identitasC(n, k) = C(n, n-k). - Hasil
pascal_C(6, 3) rekursif = 20: identitas PascalC(6,3) = C(5,2) + C(5,3) = 10 + 10 = 20. Cocok denganF.C(6, 3) = 20. - Hasil
C(10^6, 5) mod 1e9+7 = 287783408: perhitungan modular precomputeO(N + log p)laluO(1)query. Verifikasi dengan perhitungan manualnum · 120⁻¹ mod pmenghasilkan nilai yang sama.
Variasi dan Ekstensi #
Multinomial Coefficient #
n! / (k₁! · k₂! · ... · kₘ!) dengan k₁ + k₂ + ... + kₘ = nBanyaknya cara membagi n objek menjadi m kelompok berukuran k₁, k₂, ..., kₘ. Contoh: berapa anagram “MISSISSIPPI” = 11! / (1! · 4! · 4! · 2!).
Kombinasi dengan Pengulangan #
C(n + k - 1, k)Banyaknya cara memilih k item dari n kategori dengan pengulangan (boleh pilih kategori yang sama lebih dari sekali). Equivalen dengan menempatkan k bola identik ke n kotak berbeda (stars and bars).
Kombinasi Large n, Small k #
Untuk n sangat besar (mis. 10¹⁸) tetapi k kecil (≤ 10⁶), gunakan formula langsung:
C(n, k) = ∏_{i=0}^{k-1} (n - i) / (i + 1)Hitung secara iteratif modulo p, dengan setiap pembagian dilakukan via power(denom, p-2, p). Precompute faktorial tidak dimungkinkan bila n > 10⁷.
Aplikasi #
- Binomial coefficient: koefisien pada ekspansi
(a+b)ⁿ, jumlah subset ukurank. - DP: banyak soal DP berbasis kombinasi (mis. paths pada grid, koefisien Catalan
C(2n, n)/(n+1)). - Probabilitas: peluang menarik
ksukses darintrial (distribusi binomial). - Kompetisi: menghitung banyak cara berlatar kombinasi mod
p, sering dengan precomputeO(N)di awal.
Ringkasan #
P(n, k) = n! / (n - k)!— permutasi, urutan penting.C(n, k) = n! / (k! · (n - k)!)— kombinasi, urutan tidak penting.- Identitas Pascal:
C(n, k) = C(n-1, k-1) + C(n-1, k)— basis segitiga Pascal. - Komputasi modular: precompute
fact[]daninvfact[]dalamO(N + log p), lalu queryC(n, k) mod pdalamO(1). - Invers faktorial via Fermat:
invfact[N] = fact[N]^(p-2) mod p, lalu turun mundurinvfact[i-1] = invfact[i] · i.