17. Permutasi dan kombinasi

Bab sebelumnya mencacah dengan dua aturan dasar. Bab ini memberi nama dan rumus untuk dua pola yang paling sering muncul: memilih dengan memperhatikan urutan (permutasi) dan tanpa memperhatikan urutan (kombinasi).

Satu pertanyaan memisahkan keduanya:

Apakah menukar urutan menghasilkan hasil yang berbeda?

  • Menyusun tiga orang sebagai ketua, sekretaris, bendahara → ya, urutan penting → permutasi.
  • Memilih tiga orang untuk masuk tim → tidak, urutan tidak penting → kombinasi.

Faktorial

n! adalah hasil kali 1 sampai n, dan 0! = 1.

n n!
0 1
1 1
5 120
10 3.628.800
20 2,4 × 10¹⁸ (batas long long)

Kenapa 0! = 1: ada tepat satu cara menyusun barisan kosong, yaitu tidak menyusun apa-apa. Kesepakatan ini bukan sekadar kesepakatan — tanpa itu, rumus kombinasi di bawah akan pecah pada kasus tepi r = 0 dan r = n.

Faktorial membesar sangat cepat: 21! sudah tidak muat di long long. Karena itu soal yang memakai faktorial hampir selalu meminta jawabannya modulo 10⁹ + 7.

Permutasi

Menyusun seluruh n benda ada n! cara: pilihan pertama n, berikutnya n − 1, dan seterusnya — aturan perkalian menurun dari Bab 16.

Menyusun r dari n benda (urutan penting):

P(n, r) = n! / (n − r)! = n × (n−1) × … × (n−r+1)

Contoh: dari 10 siswa dipilih ketua, sekretaris, dan bendahara: P(10, 3) = 10 × 9 × 8 = 720.

Dari mana rumus itu: pilihan pertama punya n kemungkinan, pilihan kedua n − 1 karena satu sudah terpakai, dan seterusnya sampai r pilihan. Bentuk n! / (n − r)! hanyalah cara ringkas menuliskan perkalian menurun itu — pembagian dengan (n − r)! membuang ekor perkalian yang tidak dipakai.

Contoh kecil yang bisa didaftar seluruhnya: dari huruf A B C dipilih 2 dengan urutan penting. P(3, 2) = 3 × 2 = 6, dan memang ada enam:

AB  AC  BA  BC  CA  CB

Permutasi dengan unsur yang sama. Kalau ada benda yang tidak bisa dibedakan, bagi dengan faktorial banyaknya masing-masing:

n! / (n₁! × n₂! × … )

Contoh: susunan huruf pada kata OSNOSN (6 huruf: O×2, S×2, N×2): 6! / (2! × 2! × 2!) = 720 / 8 = 90.

Buka exercises/17/01-permutasi.cpp. Program itu menghitung susunan huruf OSNOSN dengan rumus, lalu memeriksanya dengan next_permutation yang mencacah susunan berbeda satu per satu. Ubah katanya menjadi AAB dan periksa jawabannya 3.

Kombinasi

Memilih r dari n benda tanpa memperhatikan urutan:

C(n, r) = n! / (r! × (n − r)!)

Hubungannya dengan permutasi: setiap kelompok berisi r benda bisa disusun dalam r! urutan, jadi C(n, r) = P(n, r) / r!.

Contoh: dari 10 siswa dipilih 3 untuk tim: C(10, 3) = 720 / 6 = 120.

Kembali ke contoh A B C tadi. Kalau urutan tidak dipedulikan, keenam susunan tadi menciut menjadi tiga kelompok:

Kombinasi Susunan yang mewakilinya
A B AB, BA
A C AC, CA
B C BC, CB

Setiap kombinasi diwakili 2! = 2 susunan, jadi C(3, 2) = 6 / 2 = 3. Angka pembagi r! pada rumus kombinasi persis jumlah susunan yang dianggap sama itu.

Cara memeriksa cepat apakah sebuah soal permutasi atau kombinasi: tukar posisi dua benda yang sudah dipilih. Kalau hasilnya dianggap berbeda, itu permutasi; kalau dianggap sama, kombinasi.

Sifat yang sering dipakai:

Sifat Arti
C(n, 0) = C(n, n) = 1 satu cara memilih semua atau tidak satu pun
C(n, r) = C(n, n − r) memilih r sama dengan membuang n−r
C(n, r) = C(n−1, r−1) + C(n−1, r) segitiga Pascal
C(n,0) + C(n,1) + … + C(n,n) = 2ⁿ seluruh himpunan bagian

Baris ketiga adalah yang paling berguna untuk program: ia mengubah kombinasi menjadi dynamic programming. Bacanya: benda ke-n ikut dipilih (sisa r−1 dari n−1) atau tidak ikut (r dari n−1).

Perhatikan bentuknya — "ambil atau tidak ambil, lalu jumlahkan kedua kemungkinan" — persis bentuk transisi knapsack di Bab 13. Bedanya hanya knapsack mengambil yang terbesar, sedangkan cacahan menjumlahkan. Bila dua jalur pada sebuah soal tidak pernah menghasilkan hal yang sama, menjumlahkan keduanya adalah cara mencacahnya.

Segitiga Pascal

n=0             1
n=1           1   1
n=2         1   2   1
n=3       1   3   3   1
n=4     1   4   6   4   1
n=5   1   5  10  10   5   1

Setiap angka adalah jumlah dua angka di atasnya. Dalam kode:

long long C[1005][1005];

for (int n = 0; n <= batas; n++) {
    C[n][0] = 1;
    for (int r = 1; r <= n; r++)
        C[n][r] = (C[n-1][r-1] + C[n-1][r]) % MOD;
}

Cara ini O(n²) dan tidak memerlukan pembagian sama sekali — cocok untuk n sampai beberapa ribu. Karena tidak ada pembagian, ia juga bekerja untuk modulus apa pun, bahkan yang bukan bilangan prima.

Perhatikan C[n][0] = 1 yang disetel di setiap baris sebelum perulangan dalam. Tanpa baris itu, seluruh segitiga berisi nol. Perhatikan juga bahwa r hanya berjalan sampai n: sisanya tetap nol, dan memang C(n, r) = 0 untuk r > n.

Buka exercises/17/02-pascal.cpp. Jalankan untuk n = 10, lalu jumlahkan satu baris dengan tangan dan bandingkan dengan 2ⁿ untuk baris itu. Cocok karena seluruh himpunan bagian dari n benda terbagi menurut ukurannya.

Kombinasi modulo untuk n besar

Untuk n sampai sejuta, tabel Pascal terlalu besar (10¹² sel). Yang dipakai adalah rumus faktorial, dengan pembagian diganti perkalian invers modulo (Bab 14):

long long fakt[1000005];

void siapkan(int n) {
    fakt[0] = 1;
    for (int i = 1; i <= n; i++) fakt[i] = fakt[i-1] * i % MOD;
}

long long kombinasi(int n, int r) {
    if (r < 0 || r > n) return 0;
    long long atas = fakt[n];
    long long bawah = fakt[r] * fakt[n-r] % MOD;
    return atas * pangkat_mod(bawah, MOD - 2, MOD) % MOD;
}

Inilah pemakaian teorema Fermat kecil yang paling sering di OSN: tanpa invers modulo, rumus kombinasi tidak bisa dihitung.

Jangan menghitung fakt[n] / (fakt[r] * fakt[n-r]) dengan pembagian biasa setelah di-modulo. Nilai fakt yang tersimpan sudah berupa sisa, dan pembagian bulat atasnya menghasilkan angka yang sama sekali tidak berhubungan dengan jawaban.

Buka exercises/17/03-kombinasi-mod.cpp. Program itu menghitung C(n, r) dengan tiga cara — Pascal, faktorial + invers, dan pembagian biasa yang salah — lalu membandingkannya. Jalankan untuk n = 30, r = 15.

Pola soal

Jalur di papan. Dari pojok kiri atas ke pojok kanan bawah papan b × k, hanya boleh melangkah ke kanan atau ke bawah. Setiap jalur terdiri dari b−1 langkah bawah dan k−1 langkah kanan; jawabannya tinggal memilih posisi langkah bawah di antara seluruh langkah:

C((b−1) + (k−1), b−1)

Contoh kecil untuk papan 2 × 3: setiap jalur terdiri dari 1 langkah bawah dan 2 langkah kanan, jadi jawabannya C(3, 1) = 3. Ketiganya bisa didaftar sebagai urutan langkah:

kanan kanan bawah
kanan bawah kanan
bawah kanan kanan

Inti gagasannya: sebuah jalur sepenuhnya ditentukan oleh di langkah keberapa saja kamu bergerak ke bawah. Jadi mencacah jalur sama dengan memilih posisi langkah bawah di antara seluruh langkah — dan itu persis kombinasi.

Memilih tim. "Berapa cara memilih 5 dari 12 pemain" → C(12, 5).

Membagi menjadi dua kelompok. "Berapa cara membagi 10 orang menjadi dua kelompok berisi 4 dan 6" → C(10, 4).

Himpunan bagian. "Berapa himpunan bagian dari n benda" → 2ⁿ, yang juga merupakan jumlah satu baris segitiga Pascal.

Pekerjaan rumah

Soal 1 — Hitung di kertas

Tulis jawaban dan caranya:

a. Berapa susunan berbeda dari huruf-huruf kata BUKU? b. Dari 8 siswa dipilih 3 untuk lomba. Berapa cara kalau ketiganya punya tugas berbeda? Berapa kalau tugasnya sama? c. Berapa jalur dari pojok kiri atas ke pojok kanan bawah papan 3 × 4 kalau hanya boleh ke kanan dan ke bawah? d. Sebuah kelas berisi 10 siswa. Berapa cara memilih paling sedikit satu siswa (kelompok boleh berapa pun ukurannya)?

Soal 2 — Segitiga Pascal

Buka exercises/17/homework/02-pascal.cpp. Baca n (≤ 30), cetak segitiga Pascal baris 0 sampai n, satu baris per baris keluaran, angka dipisahkan spasi tanpa spasi di akhir.

Soal 3 — Kombinasi modulo

Buka exercises/17/homework/03-kombinasi.cpp. Baca q (≤ 100.000) pertanyaan, masing-masing berisi n dan r (n ≤ 1.000.000). Cetak C(n, r) mod 1.000.000.007 untuk setiap pertanyaan. Siapkan tabel faktorial sekali di awal.

Soal 4 — Jalur di papan

Buka exercises/17/homework/04-jalur.cpp. Baca b dan k (masing-masing ≤ 500.000). Cetak banyaknya jalur dari pojok kiri atas ke pojok kanan bawah modulo 1.000.000.007.

Tantangan — Periksa rumusmu

Buka exercises/17/homework/05-periksa-rumus.cpp. Tulis dua fungsi: rumus(n, r) memakai segitiga Pascal, dan cacah(n, r) yang mencacah langsung seluruh himpunan bagian berukuran r dari n benda memakai mask (Bab 11). Bandingkan keduanya untuk semua n ≤ 15 dan 0 ≤ r ≤ n. Lalu tambahkan pemeriksaan ketiga: jumlah seluruh C(n, r) untuk satu n harus sama dengan 2ⁿ.

Kesalahan yang paling sering

Kesalahan Akibatnya
memakai permutasi padahal urutan tidak penting jawaban r! kali terlalu besar
lupa membagi dengan faktorial unsur yang sama susunan yang sama terhitung berkali-kali
menghitung n! langsung untuk n ≥ 21 meluap; hitung modulo sejak awal
membagi hasil yang sudah di-modulo jawaban tidak berhubungan sama sekali; pakai invers
tabel Pascal untuk n sejuta memorinya 10¹² sel; pakai faktorial + invers
lupa C(n, r) = 0 saat r > n atau r < 0 membaca faktorial di indeks negatif

Tips OSN

Bedakan tiga pertanyaan yang bunyinya mirip. Hampir semua kesalahan pada soal kombinatorika berasal dari salah memilih di antara ketiganya:

Pertanyaan pada soal Rumus
berapa cara mengurutkan n benda n!
berapa cara memilih k dari n, urutan penting n! / (n−k)!
berapa cara memilih k dari n, urutan tidak penting C(n, k)

Kata kuncinya: kalau menukar dua pilihan menghasilkan susunan yang dianggap berbeda, urutannya penting.

Siapkan faktorial sekali di awal. Untuk soal dengan banyak pertanyaan, hitung fakt[0..n] satu kali, lalu setiap kombinasi menjadi satu perkalian dan satu invers. Menghitung ulang faktorial di dalam perulangan pertanyaan adalah penyebab Time Limit Exceeded yang paling sering pada bab ini.

Pembagian modulo selalu berarti invers. C(n, k) mod p tidak boleh dihitung dengan /. Untuk p prima, inversnya pangkat(x, p − 2). Kalau pertanyaannya sangat banyak, siapkan juga array invers faktorial supaya setiap pertanyaan tidak memerlukan pangkat modulo tersendiri.

Periksa rumus dengan pencacah brute force. Untuk n ≤ 10, semua susunan bisa dicacah dengan next_permutation atau perulangan bitmask. Membandingkan rumus dengan pencacah pada n kecil menemukan kesalahan faktor dalam hitungan detik.

Kenali bentuk sekat-dan-bintang. Kalimat "bagikan N benda identik kepada K orang" hampir selalu berarti kombinasi: dengan syarat setiap orang mendapat sedikitnya satu, jawabannya C(N − 1, K − 1); kalau boleh ada yang tidak kebagian, C(N + K − 1, K − 1). Bedanya satu kata pada soal.

Batas atas faktorial jauh lebih kecil daripada dugaan. 20! sudah mendekati 2,4 × 10¹⁸ dan 21! melewati long long. Jadi soal yang meminta faktorial tanpa modulo hampir pasti membatasi n ≤ 20 — dan kalau batasannya lebih besar, modulo pasti diminta.

C(n, k) bernilai 0 kalau k > n atau k < 0. Rumus faktorial tidak memberi tahu hal itu; ia justru membaca fakt[n − k] dengan indeks negatif. Periksa syaratnya sebelum rumusnya dipakai.

Ringkasan

  • Satu pertanyaan memisahkan keduanya: apakah menukar urutan menghasilkan hasil yang berbeda? Ya → permutasi, tidak → kombinasi.
  • P(n, r) = n! / (n − r)! adalah perkalian menurun n × (n−1) × … sebanyak r faktor.
  • C(n, r) = P(n, r) / r!, karena setiap kelompok berisi r benda bisa disusun dalam r! urutan.
  • Permutasi dengan unsur yang sama: bagi dengan faktorial banyaknya masing-masing unsur.
  • C(n, r) = C(n−1, r−1) + C(n−1, r) adalah segitiga Pascal, bentuk DP-nya, dan cocok untuk n sampai beberapa ribu.
  • Untuk n besar, pakai tabel faktorial modulo dan invers dari teorema Fermat kecil (Bab 14).
  • Jalur pada papan yang hanya boleh ke kanan dan ke bawah adalah soal kombinasi: pilih di langkah mana saja bergerak ke bawah.

Latihan bergaya OSN

Ketiga soal berikut memakai kombinasi dengan tiga penyamaran berbeda: permutasi dengan unsur berulang, sekat-dan-bintang, dan pemilihan dengan pantangan. Kenali bentuknya lebih dulu, baru tulis rumusnya.

Kerangka ketiganya ada di exercises/17/osn/.

Soal 1 — Susunan Huruf

Diberikan sebuah kata S. Hitung ada berapa kata berbeda yang bisa dibentuk dengan menyusun ulang seluruh huruf S. Dua susunan dianggap sama kalau hurufnya sama persis pada setiap posisi.

Keluarkan jawabannya modulo 1.000.000.007.

Format masukan. Satu baris berisi kata S yang seluruhnya huruf kecil a sampai z.

Format keluaran. Satu baris berisi banyaknya susunan berbeda modulo 1.000.000.007.

Batasan. 1 ≤ panjang S ≤ 10⁶.

Contoh masukan

aabb

Contoh keluaran

6

Penjelasan. Susunannya aabb, abab, abba, baab, baba, dan bbaa. Kalau keempat hurufnya dianggap berbeda, susunannya 4! = 24; tetapi menukar dua a tidak menghasilkan kata baru, begitu juga dua b, sehingga setiap kata terhitung 2! × 2! = 4 kali.

Kerangkanya: exercises/17/osn/01-susunan-huruf.cpp

Soal 2 — Bagi Permen

Seorang guru punya N permen yang bentuknya sama persis dan akan membagikannya kepada K anak. Setiap anak harus mendapat sedikitnya satu permen, dan seluruh permen harus habis dibagikan.

Dua pembagian dianggap berbeda kalau ada anak yang menerima jumlah permen berbeda. Hitung banyaknya pembagian yang mungkin, modulo 1.000.000.007.

Format masukan. Satu baris berisi dua bilangan bulat N dan K.

Format keluaran. Satu baris berisi banyaknya pembagian modulo 1.000.000.007. Kalau tidak ada pembagian yang mungkin, cetak 0.

Batasan.

  • 1 ≤ N ≤ 10⁶
  • 1 ≤ K ≤ 10⁶

Contoh masukan

5 2

Contoh keluaran

4

Penjelasan. Pembagiannya (1, 4), (2, 3), (3, 2), dan (4, 1). Karena anaknya bisa dibedakan, (1, 4) dan (4, 1) dihitung sebagai dua pembagian berbeda — sedangkan permennya identik, sehingga yang membedakan hanya jumlahnya.

Kerangkanya: exercises/17/osn/02-bagi-permen.cpp

Soal 3 — Pilih Tanpa Bertetangga

Di sebuah ruangan terdapat N kursi yang berjajar dalam satu baris. Panitia ingin menempatkan K peserta, dan demi mencegah contek-mencontek, tidak boleh ada dua peserta yang duduk di kursi bersebelahan.

Hitung ada berapa cara memilih kursi yang akan diduduki, modulo 1.000.000.007. Pesertanya dianggap tidak dibedakan — yang dihitung hanya kumpulan kursinya.

Format masukan. Satu baris berisi dua bilangan bulat N dan K.

Format keluaran. Satu baris berisi banyaknya cara modulo 1.000.000.007. Kalau tidak ada cara yang mungkin, cetak 0.

Batasan.

  • 1 ≤ N ≤ 10⁶
  • 0 ≤ K ≤ 10⁶

Contoh masukan

5 2

Contoh keluaran

6

Penjelasan. Pilihan kursinya (1,3), (1,4), (1,5), (2,4), (2,5), dan (3,5). Pilihan seperti (2,3) tidak sah karena kursinya bersebelahan.

Kerangkanya: exercises/17/osn/03-pilih-tanpa-bertetangga.cpp

Bingung atau sudah selesai? Buka halaman pembahasan PR.