11. Brute force
Brute force berarti mencoba semua kemungkinan lalu memilih yang memenuhi syarat. Ia sering diremehkan, padahal di OSN brute force punya tiga kegunaan: menjadi jawaban penuh untuk soal berbatasan kecil, menjadi jawaban sebagian untuk soal berbatasan besar, dan menjadi pembanding untuk menguji penyelesaian yang lebih pintar.
Langkah menyusun brute force
- Tentukan apa yang dicoba — semua elemen? semua pasangan? semua himpunan bagian? semua urutan?
- Hitung berapa banyak kemungkinannya.
- Bandingkan dengan tabel batasan di Bab 10.
- Kalau muat, tulis perulangannya. Kalau tidak, cari pengamatan yang memangkas jumlah kemungkinan.
Langkah 2 dan 3 tidak boleh dilewati. Brute force yang tidak dihitung dulu adalah penyebab paling umum jawaban Time Limit Exceeded.
Banyaknya kemungkinan untuk keempat bentuk yang dibahas bab ini:
| Yang dicoba | Banyaknya kemungkinan | Aman sampai kira-kira |
|---|---|---|
| semua elemen | n |
n = 10.000.000 |
| semua pasangan | n(n−1)/2 | n = 5.000 |
| semua tripel | n³/6 | n = 500 |
| semua himpunan bagian | 2ⁿ | n = 22 |
| semua urutan | n! | n = 10 |
Betapa cepatnya dua baris terakhir membesar:
n |
2ⁿ | n! |
|---|---|---|
| 10 | 1.024 | 3.628.800 |
| 15 | 32.768 | 1,3 × 10¹² |
| 20 | 1.048.576 | 2,4 × 10¹⁸ |
| 25 | 33.554.432 | 1,6 × 10²⁵ |
| 30 | 1.073.741.824 | 2,7 × 10³² |
Itulah sebabnya batasan soal adalah petunjuk yang hampir tidak pernah
berbohong: n ≤ 20 hampir selalu berarti "coba semua
himpunan bagian", dan n ≤ 10 hampir selalu berarti "coba
semua urutan".
Bentuk 1: semua elemen
Kemungkinannya n, jadi O(n). Ini brute force paling
sederhana — dan sering sudah merupakan penyelesaian terbaik.
// cari elemen terbesar
int besar = a[0];
for (int i = 1; i < n; i++) if (a[i] > besar) besar = a[i];Bentuk 2: semua pasangan
Kemungkinannya n(n−1)/2, jadi O(n²). Aman sampai sekitar
n = 5.000.
// berapa pasangan yang jumlahnya k?
int banyak = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (a[i] + a[j] == k) banyak++;Perhatikan j = i + 1, bukan j = 0. Dengan
begitu setiap pasangan dihitung sekali dan elemen tidak dipasangkan
dengan dirinya sendiri. Untuk n = 4, pasangan yang
diperiksa persis enam:
i |
j yang dicoba |
pasangan |
|---|---|---|
| 0 | 1, 2, 3 | (0,1) (0,2) (0,3) |
| 1 | 2, 3 | (1,2) (1,3) |
| 2 | 3 | (2,3) |
| 3 | — | — |
Kalau j dimulai dari 0, jumlahnya menjadi
16: setiap pasangan terhitung dua kali dan setiap elemen dipasangkan
dengan dirinya sendiri. Pada soal "berapa banyak pasangan", dua
kesalahan itu masing-masing mengubah jawaban.
Buka
exercises/11/01-semua-pasangan.cpp.
Ubah j = i + 1 menjadi j = 0, jalankan untuk
data yang sama, dan bandingkan cacahannya dengan yang tadi.
Bentuk 3: semua himpunan bagian
Kalau setiap barang hanya punya dua keadaan — diambil atau tidak —
maka n barang punya 2ⁿ kemungkinan. Aman sampai sekitar
n = 22.
Cara menuliskannya memakai bilangan biner: bilangan mask
dari 0 sampai 2ⁿ − 1 mewakili satu pilihan,
dan bit ke-i menyatakan apakah barang ke-i
diambil.
for (int mask = 0; mask < (1 << n); mask++) {
long long jumlah = 0;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) jumlah += a[i]; // bit ke-i menyala
}
if (jumlah == target) banyak++;
}| Tulisan | Arti |
|---|---|
1 << n |
2 pangkat n |
mask & (1 << i) |
bernilai tidak nol jika bit ke-i menyala |
Cara membacanya: setiap bilangan bulat sekaligus merupakan daftar
"ya/tidak". Untuk n = 3 dan barang
a = {5, 7, 2}, kedelapan nilai mask mewakili
kedelapan pilihan:
mask |
biner | barang yang diambil | jumlah |
|---|---|---|---|
| 0 | 000 | — | 0 |
| 1 | 001 | a[0] |
5 |
| 2 | 010 | a[1] |
7 |
| 3 | 011 | a[0], a[1] |
12 |
| 4 | 100 | a[2] |
2 |
| 5 | 101 | a[0], a[2] |
7 |
| 6 | 110 | a[1], a[2] |
9 |
| 7 | 111 | semuanya | 14 |
Perhatikan bahwa bit paling kanan adalah bit ke-0,
yaitu barang pertama. Kolom biner itu persis hitungan biner dari 0
sampai 7, jadi perulangan
for (int mask = 0; mask < (1 << n); mask++)
dijamin melewati setiap kemungkinan tepat satu kali — tidak ada yang
terlewat dan tidak ada yang dobel. Itulah keunggulan cara ini dibanding
menulis perulangan bersarang sebanyak n tingkat, yang
bahkan tidak mungkin ditulis kalau n baru diketahui saat
program berjalan.
1 << n dihitung sebagai int. Untuk
n sampai 30 masih aman, tetapi 1 << 31
sudah meluap. Kalau n bisa sebesar itu, tulis
1LL << n — walaupun pada praktiknya
n = 31 sudah jauh terlalu lambat untuk dicoba semuanya.
Jumlah langkahnya n · 2ⁿ. Untuk n = 20 itu sekitar 21
juta — masih aman. Untuk n = 30 menjadi 32 miliar —
tidak.
Buka
exercises/11/02-himpunan-bagian.cpp.
Jalankan untuk n = 4 dan perhatikan keluarannya mencetak
keenam belas himpunan bagian. Lalu ubah n menjadi 20 dan
ukur waktunya.
Bentuk 4: semua urutan
Kalau yang dicari susunan (siapa lebih dulu, jalur mana yang
ditempuh), jumlah kemungkinannya n!. Aman hanya sampai sekitar
n = 10 (10! = 3,6 juta).
next_permutation dari <algorithm>
mengubah isinya menjadi urutan berikutnya menurut
kamus, dan mengembalikan false begitu tidak ada
lagi urutan sesudahnya. Untuk {0, 1, 2} urutan yang
dihasilkannya:
012 021 102 120 201 210
Karena ia hanya melangkah "maju", datanya harus dimulai dalam keadaan terurut menaik — kalau tidak, sebagian urutan tidak pernah muncul:
#include <algorithm>
int p[] = {0, 1, 2, 3};
sort(p, p + n);
do {
// p sekarang berisi satu urutan
} while (next_permutation(p, p + n));Memangkas kemungkinan
Brute force tidak harus benar-benar memeriksa semuanya. Dua cara memangkas:
Berhenti lebih awal. Begitu jawaban sudah pasti, keluar.
for (long long i = 2; i * i <= n; i++)
if (n % i == 0) return false; // tidak perlu diteruskanMenghitung sisanya, bukan mencarinya. Kalau dua di antara tiga nilai sudah dipilih, nilai ketiga sering bisa dihitung langsung — satu perulangan bersarang hilang.
// a + b + c = k, cari banyaknya tripel: cukup dua perulangan
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++) {
int c = k - a[i] - a[j];
// lalu cari c dengan cara yang cepat
}Buka
exercises/11/03-semua-urutan.cpp.
Ganti baris yang mengisi p beserta baris
sort-nya menjadi
p[0] = 2; p[1] = 0; p[2] = 1;, lalu jalankan untuk
n = 3. Yang tercetak hanya dua urutan, bukan enam:
next_permutation hanya bisa melangkah maju dari susunan
awalnya.
Pemangkasan mengubah O(n³) menjadi O(n²), tetapi tidak mengubah O(2ⁿ) menjadi O(n²). Kalau batasan soal jauh di luar jangkauan brute force, yang dibutuhkan bukan pemangkasan melainkan ide lain — greedy (Bab 12) atau dynamic programming (Bab 13).
Nilai sebagian
Soal OSN sering berbatasan bertingkat, misalnya:
- Subsoal 1 (30 poin):
n ≤ 20 - Subsoal 2 (30 poin):
n ≤ 2.000 - Subsoal 3 (40 poin):
n ≤ 200.000
Brute force O(2ⁿ) menjawab subsoal 1. Brute force O(n²) menjawab subsoal 1 dan 2. Enam puluh poin dari penyelesaian yang "bodoh" jauh lebih baik daripada nol poin dari penyelesaian pintar yang tidak selesai ditulis.
Strategi lomba: tulis brute force lebih dulu, kirim, amankan poinnya, baru pikirkan penyelesaian penuh. Brute force itu juga langsung berguna sebagai pembanding (Bab 10) saat mencari bug di penyelesaian cepat.
Contoh lengkap: jumlah himpunan bagian
Diberikan
nbarang dengan berata[1..n]dan sebuah targetk. Berapa banyak cara memilih sebagian barang (boleh nol barang) sehingga total beratnya tepatk? Batasan:n ≤ 20,a[i] ≤ 10⁹.
Berapa kemungkinannya? Setiap barang diambil atau
tidak: 2²⁰ ≈ 1 juta. Setiap kemungkinan perlu n langkah
untuk menjumlahkan: 21 juta. Masuk.
Kodenya:
int banyak = 0;
for (int mask = 0; mask < (1 << n); mask++) {
long long jumlah = 0;
for (int i = 0; i < n; i++)
if (mask & (1 << i)) jumlah += a[i];
if (jumlah == k) banyak++;
}
cout << banyak << "\n";Kalau batasannya n ≤ 40, cara ini mati — dan
penyelesaiannya adalah membelah data menjadi dua bagian, masing-masing
2²⁰. Teknik itu di luar cakupan buku ini, tetapi pola pikirnya sama:
hitung dulu, baru putuskan.
Pekerjaan rumah
Soal 1 — Pasangan berjumlah k
Buka
exercises/11/homework/01-pasangan.cpp.
Baca n (≤ 2.000), k, lalu n
bilangan. Cetak banyaknya pasangan indeks (i, j) dengan
i < j dan a[i] + a[j] == k.
Soal 2 — Tripel Pythagoras
Buka
exercises/11/homework/02-tripel.cpp.
Baca n (≤ 300). Cetak banyaknya tripel
(a, b, c) dengan 1 ≤ a < b < c ≤ n dan
a² + b² = c². Hitung dulu jumlah langkah tiga perulangan
bersarang untuk n = 300, tulis di komentar, baru tulis
kodenya.
Soal 3 — Himpunan bagian berjumlah k
Buka
exercises/11/homework/03-himpunan.cpp.
Baca n (≤ 20), k, lalu n
bilangan. Cetak banyaknya cara memilih sebagian barang yang jumlahnya
tepat k. Himpunan kosong dihitung kalau
k = 0.
Soal 4 — Urutan terbaik
Buka
exercises/11/homework/04-urutan.cpp.
Baca n (≤ 8) dan sebuah matriks jarak n × n.
Cetak panjang jalur terpendek yang mengunjungi seluruh kota tepat
sekali, dimulai dari kota 0. Pakai next_permutation pada
kota 1..n-1.
Tantangan — Nilai sebagian
Buka
exercises/11/homework/05-subsoal.cpp.
Soalnya: cetak selisih terbesar a[j] − a[i] dengan
i < j (semacam "untung terbesar membeli lalu
menjual"). Tulis dua fungsi: versi O(n²) yang memeriksa
semua pasangan, dan versi O(n) yang menyimpan nilai terkecil sejauh ini.
Bandingkan keduanya dengan penguji acak seperti Bab 10, lalu ukur
waktunya untuk n = 50.000.
Kesalahan yang paling sering
| Yang ditulis | Akibatnya |
|---|---|
j = 0 padahal pasangan tak berurut |
setiap pasangan terhitung dua kali |
| lupa memasukkan himpunan kosong | jawaban kurang satu saat k = 0 |
mask < (1 << n) ditulis
mask <= (1 << n) |
satu kemungkinan palsu ikut terhitung |
penampung jumlah bertipe int |
meluap pada data besar |
lupa sort sebelum next_permutation |
sebagian urutan tidak pernah dicoba |
| brute force ditulis tanpa dihitung dulu | Time Limit Exceeded setelah kodenya jadi |
Tips OSN
Batasan kecil adalah undangan terbuka. Angka-angka berikut hampir selalu berarti "coba semua kemungkinan", dan penulis soal memang meletakkannya di situ supaya brute force muat:
| Batasan terlihat | Yang dimaksud penulis soal | Jumlah kemungkinan |
|---|---|---|
N ≤ 10 |
semua urutan | 10! = 3.628.800 |
N ≤ 20 |
semua himpunan bagian | 2²⁰ ≈ 10⁶ |
N ≤ 500 |
semua tripel | ≈ 2 × 10⁷ |
N ≤ 5.000 |
semua pasangan | ≈ 1,25 × 10⁷ |
Kalau batasannya terlihat "terlalu kecil", jangan curiga — itu justru jawabannya.
Brute force adalah jawaban yang sah, bukan jawaban memalukan. Pada soal bersubsoal, jawaban brute force menyelesaikan subsoal berbatas kecil dan memberi nilai nyata. Peserta yang menghabiskan seluruh waktunya mencari jawaban sempurna dan tidak mengirim apa pun mendapat nol.
Tulis brute force lebih dulu walaupun kamu tahu cara cepatnya. Ia menjadi pembanding untuk menguji jawaban cepat pada masukan acak, seperti pada bagian Menguji dengan pembanding di Bab 10. Banyak kesalahan halus pada jawaban cepat hanya bisa ditemukan dengan cara ini.
Susun perulangan supaya tidak menghitung dua kali.
Untuk memilih pasangan atau tripel dari posisi yang berbeda, tulis
j mulai dari i + 1 dan k mulai
dari j + 1. Cara ini sekaligus menghindarkan pembagian di
akhir dan menjamin setiap kombinasi terhitung tepat sekali.
Pangkas sedini mungkin. Pemeriksaan yang bisa dilakukan di perulangan luar jangan ditunda ke perulangan dalam. Memindahkan satu syarat dari tingkat terdalam ke tingkat kedua sering memotong waktu jalan sampai sepuluh kali lipat, tanpa mengubah jawabannya sama sekali.
Kenali batas atas jawaban sebelum memilih tipe.
Brute force sering menjumlahkan sangat banyak kemungkinan. Jumlah tripel
pada N = 500 saja sudah 20 juta; kalau yang dihitung
nilainya, bukan cacahnya, long long hampir selalu
diperlukan.
Bentuk
for (int mask = 0; mask < (1 << n); mask++) adalah
cara baku mencacah semua himpunan bagian. Bit ke-i pada
mask menyala berarti elemen ke-i ikut dipilih,
diperiksa dengan mask & (1 << i). Untuk
n = 20 itu 1.048.576 himpunan bagian — satu detik lebih
dari cukup.
Ringkasan
- Brute force berarti mencoba semua kemungkinan; ia berguna sebagai jawaban penuh untuk batasan kecil, jawaban sebagian untuk batasan besar, dan pembanding untuk menguji penyelesaian cepat.
- Urutan kerjanya: tentukan apa yang dicoba, hitung banyaknya, bandingkan dengan tabel batasan Bab 10, baru tulis kodenya.
- Empat bentuk bakunya: semua elemen O(n), semua pasangan O(n²), semua himpunan bagian O(2ⁿ), semua urutan O(n!).
- Himpunan bagian ditulis dengan
maskbiner;1 << nadalah 2ⁿ danmask & (1 << i)memeriksa bit ke-i. - Semua urutan dihasilkan
next_permutation, dan datanya harus terurut lebih dulu. - Pemangkasan bisa menghapus satu tingkat perulangan, tetapi tidak bisa mengubah 2ⁿ menjadi n². Kalau batasannya jauh di luar jangkauan, yang dibutuhkan ide lain — Bab 12 atau Bab 13.
Latihan bergaya OSN
Ketiga soal berikut adalah tiga bentuk brute force yang paling sering muncul: semua tripel, semua himpunan bagian, dan semua urutan. Perhatikan batasannya — masing-masing dipilih supaya tepat muat.
Kerangka ketiganya ada di
exercises/11/osn/.
Soal 1 — Tripel Berjumlah Nol
Diberikan N bilangan bulat. Hitung ada berapa tripel
posisi (i, j, k) dengan i < j < k
sehingga Aᵢ + A_j + A_k = 0.
Format masukan. Baris pertama berisi bilangan bulat
N. Baris kedua berisi N bilangan bulat.
Format keluaran. Satu baris berisi banyaknya tripel.
Batasan.
- 3 ≤
N≤ 500 - −10⁹ ≤
Aᵢ≤ 10⁹
Contoh masukan
6
-1 0 1 2 -1 -4
Contoh keluaran
3
Penjelasan. Tripel yang berjumlah nol adalah posisi (1, 2, 3) yang berisi −1, 0, 1; posisi (2, 3, 5) yang berisi 0, 1, −1; dan posisi (1, 4, 5) yang berisi −1, 2, −1. Yang dihitung adalah tripel posisi, jadi dua tripel yang isinya sama tetapi posisinya berbeda dihitung dua kali.
Kerangkanya:
exercises/11/osn/01-tripel-nol.cpp
Soal 2 — Himpunan Bagian Terdekat
Seorang panitia punya N kardus dengan berat berbeda dan
ingin memuat sebagian ke dalam mobil yang daya angkutnya kira-kira
S. Ia boleh memilih kardus mana pun, termasuk tidak memilih
sama sekali.
Tentukan total berat yang paling dekat ke S — boleh
kurang, boleh lebih. Kalau ada dua pilihan yang jaraknya ke
S sama, pilih total berat yang lebih kecil.
Format masukan. Baris pertama berisi dua bilangan
bulat N dan S. Baris kedua berisi
N bilangan bulat.
Format keluaran. Satu baris berisi total berat yang
paling dekat ke S.
Batasan.
- 1 ≤
N≤ 20 - 1 ≤
S≤ 10¹⁸ - 1 ≤
Aᵢ≤ 10⁹
Contoh masukan
4 10
3 34 4 12
Contoh keluaran
12
Penjelasan. Total yang mungkin antara lain 0, 3, 4, 7, 12, 15, 16, dan seterusnya. Yang paling dekat ke 10 adalah 12, yang berjarak 2; pilihan 7 berjarak 3.
Kerangkanya:
exercises/11/osn/02-himpunan-terdekat.cpp
Soal 3 — Tur Terpendek
Seorang kurir harus berangkat dari kota pertama, mengunjungi semua kota tepat satu kali, lalu kembali ke kota pertama. Jarak antar kota diberikan dalam bentuk tabel. Tentukan total jarak terpendek yang mungkin.
Format masukan. Baris pertama berisi bilangan bulat
N. N baris berikutnya masing-masing berisi
N bilangan bulat; bilangan ke-j pada baris
ke-i adalah jarak dari kota ke-i ke kota
ke-j.
Format keluaran. Satu baris berisi total jarak terpendek.
Batasan.
- 2 ≤
N≤ 8 - 0 ≤ jarak ≤ 1000, dan jarak dari sebuah kota ke dirinya sendiri adalah 0.
Contoh masukan
4
0 10 15 20
10 0 35 25
15 35 0 30
20 25 30 0
Contoh keluaran
80
Penjelasan. Salah satu tur terpendek adalah kota 1 → 2 → 4 → 3 → 1, dengan total 10 + 25 + 30 + 15 = 80.
Kerangkanya:
exercises/11/osn/03-tur-terpendek.cpp
Bingung atau sudah selesai? Buka halaman pembahasan PR.