11. Brute force — Pembahasan PR
Berkas .cpp jawabannya ada di
exercises/11/homework/solutions/.
Halaman ini dibuka sesudah mencoba sendiri. Bandingkan jalan pikiranmu dengan bagian Cara berpikir, bukan kodenya baris per baris: dua kode yang berbeda bentuk bisa sama-sama benar, dan yang perlu ditiru adalah cara sampai ke sana. Bagian Uji di tiap soal berisi masukan dan keluaran yang sudah diperiksa — jalankan jawabanmu sendiri dengan masukan itu sebelum merasa selesai.
Menjalankan salah satu jawabannya:
g++ -std=c++17 -O2 -o jawab exercises/11/homework/solutions/01-pasangan.cpp
./jawab
Soal 1 — Pasangan berjumlah k
Soal. Cacah pasangan (i, j),
i < j, dengan a[i] + a[j] == k.
Cara berpikir. Hitung dulu: n ≤ 2.000
berarti n²/2 = 2 juta pasangan. Aman. Brute force dua perulangan sudah
merupakan jawaban penuh.
Kunci kebenarannya ada pada j = i + 1. Kalau ditulis
j = 0, setiap pasangan terhitung dua kali dan elemen ikut
dipasangkan dengan dirinya sendiri.
Pembahasan.
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++;Uji.
| Masukan | Keluaran | Catatan |
|---|---|---|
5 10 / 3 7 2 8 5 |
2 |
(3,7) dan (2,8) |
4 6 / 3 3 3 3 |
6 |
semua pasangan dari empat angka sama |
1 5 / 5 |
0 |
satu elemen tidak bisa berpasangan |
Baris kedua adalah kasus tepi yang penting: nilai yang sama tidak
boleh membuat program berhenti menghitung. Jawabannya 6, yaitu seluruh
pasangan 4 × 3 / 2. Baris ketiga menguji
n = 1, saat perulangan dalam tidak pernah berjalan.
Kesalahan umum.
- Memakai
intuntuk pencacah ketika soal memungkinkan jawaban lebih dari 2 × 10⁹ (misalnya semua elemen sama dankpas). Untukn = 2.000maksimalnya sekitar 2 juta, jadiintcukup di sini — tetapi periksa batasnya setiap kali. - Menjumlahkan
a[i] + a[j]dalamintketika elemennya mendekati 10⁹. Dua elemen sebesar itu sudah melewati batas.
Soal 2 — Tripel Pythagoras
Soal. Cacah tripel a < b < c ≤ n
dengan a² + b² = c².
Cara berpikir. Tiga perulangan bersarang dengan
syarat a < b < c memberi sekitar n³/6 langkah. Untuk
n = 300 itu 4,5 juta — masuk dengan mudah.
Tetapi ini juga contoh pemangkasan dari bab: c tidak
perlu dicari. Setelah a dan b dipilih,
c dihitung sebagai akar dari
a² + b², lalu diperiksa apakah bilangan bulat. Jumlah
langkahnya turun menjadi n²/2 = 45.000.
Pembahasan (tiga perulangan).
int banyak = 0;
for (int a = 1; a <= n; a++)
for (int b = a + 1; b <= n; b++)
for (int c = b + 1; c <= n; c++)
if (a * a + b * b == c * c) banyak++;Pembahasan (dua perulangan).
int kuadrat = a * a + b * b;
int c = (int)sqrt((double)kuadrat);
for (int d = c - 1; d <= c + 1; d++)
if (d > b && d <= n && d * d == kuadrat) banyak++;Pemeriksaan c − 1 sampai c + 1 ada karena
sqrt bekerja dengan double dan bisa meleset
satu. Kebiasaan ini menyelamatkan banyak jawaban.
Untuk n = 20 jawabannya 6, untuk n = 300
jawabannya 209.
Uji.
| Masukan | Keluaran | Catatan |
|---|---|---|
4 |
0 |
tripel terkecil (3,4,5) belum muat |
20 |
6 |
|
300 |
209 |
angka yang diminta soal |
Versi tiga perulangan memerlukan sekitar 300³/6 ≈ 4,5 juta langkah
untuk n = 300, sedangkan versi dua perulangan (berkas
02b-tripel-cepat.cpp) sekitar 45.000. Keduanya menjawab
209; bedanya baru terasa kalau batasnya dinaikkan.
Kesalahan umum.
- Menulis
b = 1danc = 1sebagai batas bawah, lalu menghitung tripel yang sama beberapa kali dalam urutan berbeda. - Mempercayai
(int)sqrt(...)tanpa memeriksa tetangganya.
Soal 3 — Himpunan bagian berjumlah k
Soal. Cacah himpunan bagian yang jumlahnya
k, n ≤ 20.
Cara berpikir. 2²⁰ ≈ 1 juta kemungkinan, masing-masing butuh 20 langkah penjumlahan: 21 juta. Masuk.
Setiap bilangan mask dari 0 sampai 2ⁿ − 1 adalah satu
himpunan bagian. mask & (1 << i) memeriksa apakah
barang ke-i ikut terpilih.
Pembahasan.
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++;
}Uji.
| Masukan | Keluaran | Catatan |
|---|---|---|
4 5 / 1 2 3 4 |
2 |
{1,4} dan {2,3} |
3 0 / 1 2 3 |
1 |
himpunan kosong, dan hanya itu |
1 1 / 1 |
1 |
satu barang, tepat pas |
Baris kedua adalah inti soalnya: mask = 0 mewakili
"tidak mengambil apa pun" dan jumlahnya 0, jadi ia terhitung ketika
k = 0. Program yang memulai perulangan dari
mask = 1 akan menjawab 0 di sini.
Kesalahan umum.
- Menulis
1 << ndengannsampai 31 atau lebih; hasilnya meluap. Untuknbesar gunakan1LL << n. - Memakai
intuntukjumlah. Dua puluh elemen bernilai 10⁹ menghasilkan 2 × 10¹⁰. - Lupa bahwa
mask = 0adalah himpunan kosong dengan jumlah 0 — yang memang harus ikut terhitung ketikak = 0.
Soal 4 — Urutan terbaik
Soal. Jalur terpendek mengunjungi semua kota, mulai
dari kota 0, n ≤ 8.
Cara berpikir. Kota 0 tetap di awal, jadi yang
diatur urutannya hanya n − 1 kota sisanya: paling banyak 7!
= 5.040 urutan. Setiap urutan memerlukan n langkah untuk
menjumlahkan jaraknya. Sangat ringan.
next_permutation menghasilkan urutan berikutnya dan
mengembalikan false setelah urutan terakhir — karena itu
datanya harus terurut sebelum perulangan
do ... while dimulai.
Pembahasan.
int kota[16];
int m = 0;
for (int i = 1; i < n; i++) kota[m++] = i;
sort(kota, kota + m);
int terpendek = 2000000000;
do {
int panjang = 0, sekarang = 0;
for (int i = 0; i < m; i++) {
panjang += jarak[sekarang][kota[i]];
sekarang = kota[i];
}
if (panjang < terpendek) terpendek = panjang;
} while (next_permutation(kota, kota + m));Untuk contoh 4 kota pada berkas PR, jawabannya 65 (jalur 0 → 2 → 1 → 3: 15 + 35 + 25 = 75; jalur 0 → 1 → 3 → 2: 10 + 25 + 30 = 65).
Uji.
| Masukan | Keluaran |
|---|---|
2 / 0 5 / 5 0 |
5 |
4 / 0 10 15 20 / 10 0 35 25 /
15 35 0 30 / 20 25 30 0 |
65 |
Kasus kedua adalah contoh klasik: jalur terpendek yang mengunjungi
keempat kota dari kota 0 adalah 0 → 1 → 3 → 2 dengan panjang 10 + 25 +
30 = 65. Jumlah urutan yang dicoba (n−1)! = 6, cukup
sedikit untuk diperiksa dengan tangan — dan itu memang gunanya menguji
dengan n kecil.
Kesalahan umum.
- Ikut mempermutasikan kota 0, sehingga jalur yang tidak berawal di kota 0 ikut dihitung.
- Lupa
sortsebelumdo ... while, sehingga sebagian urutan terlewat. - Lupa kasus
n = 1, ketika tidak ada perpindahan sama sekali.
Tantangan — Nilai sebagian
Soal. Tulis versi O(n²) dan O(n) untuk selisih
terbesar a[j] − a[i] dengan i < j, lalu
bandingkan.
Cara berpikir. Versi lambat adalah definisi soal
yang ditulis apa adanya. Versi cepat berasal dari satu pengamatan: saat
berada di posisi j, nilai a[i] terbaik adalah
yang terkecil di antara semua posisi sebelum
j. Jadi cukup satu variabel terkecil
yang diperbarui sambil berjalan.
Urutan di dalam perulangan penting: hitung dulu selisih memakai
terkecil yang lama (yang pasti berasal dari indeks lebih
kecil), baru perbarui terkecil. Kalau dibalik,
a[j] − a[j] ikut terhitung dan syarat i < j
dilanggar.
Pembahasan.
int cepat(int a[], int n) {
int terbaik = -2000000000;
int terkecil = a[0];
for (int j = 1; j < n; j++) {
if (a[j] - terkecil > terbaik) terbaik = a[j] - terkecil;
if (a[j] < terkecil) terkecil = a[j];
}
return terbaik;
}Hasil pengukuran pada n = 50.000 (komputer penulis):
| Versi | Waktu |
|---|---|
| O(n²) | 0,57 detik |
| O(n) | 0,000024 detik |
Untuk n = 200.000, versi O(n²) memerlukan sekitar 16
kali lipat dari 0,57 detik — sekitar 9 detik, jauh melewati batas waktu.
Versi O(n) tetap sepersekian milidetik. Inilah bentuk nyata tabel
batasan Bab 10.
Uji. Program jawabannya mencetak sendiri hasil perbandingannya:
2000 uji cocok
lambat: 999886 (0.580913 detik)
cepat : 999886 (2.4e-05 detik)
Dua baris terakhir untuk n = 50.000. Jawabannya sama,
tetapi versi O(n²) memerlukan sekitar 1,25 miliar pasangan sedangkan
versi O(n) hanya 50.000 langkah — pada pengukuran ini sekitar dua puluh
ribu kali lebih cepat. Inilah bentuk nilai sebagian yang dimaksud Bab
11: versi lambat tetap benar dan tetap bernilai pada subsoal kecil.
Kesalahan umum.
- Memperbarui
terkecilsebelum menghitung selisih. - Memberi nilai awal
terbaik = 0. Untuk data menurun seperti5 3 1, jawabannya negatif dan versi itu salah.
Latihan bergaya OSN — pembahasan
Berkas jawabannya ada di
exercises/11/osn/solutions/.
Soal 1 — Tripel Berjumlah Nol
Hitung dulu. Banyaknya tripel posisi dari 500 data
adalah 500 × 499 × 498 / 6 ≈ 2,07 × 10⁷. Di bawah 10⁸, jadi tiga
perulangan bersarang muat. Diukur pada mesin penulis, jawaban di bawah
selesai dalam 0,03 detik untuk N = 500.
Cara berpikir. Satu-satunya keputusan di sini adalah
batas perulangan. Menulis ketiganya dari 0 sampai N akan
menghitung setiap tripel enam kali dan juga menghitung tripel yang
memakai posisi yang sama dua kali. Menulis j mulai dari
i + 1 dan k mulai dari j + 1
menyelesaikan keduanya sekaligus, tanpa pembagian dan tanpa pemeriksaan
tambahan.
Pembahasan.
long long banyak = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
if (a[i] + a[j] + a[k] == 0) banyak++;
}
}
}Penjumlahan tiga bilangan sebesar 10⁹ mencapai 3 × 10⁹, jadi
a harus long long. Kalau a
bertipe int, penjumlahannya meluap dan tripel yang
sebenarnya tidak berjumlah nol bisa ikut terhitung.
Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
6 / -1 0 1 2 -1 -4 |
3 |
contoh soal |
3 / 1 2 3 |
0 |
tidak ada tripel |
4 / 0 0 0 0 |
4 |
semua kombinasi nol |
Kasus ketiga berguna sebagai pemeriksaan batas perulangan: jawabannya harus 4, yaitu banyaknya cara memilih 3 posisi dari 4. Jawaban 24 berarti urutannya ikut terhitung; jawaban 64 berarti posisi yang sama boleh dipakai ulang.
Kesalahan umum.
- Memulai
jdankdari 0, sehingga tripel terhitung berkali-kali. - Memakai
intuntuk data. - Memakai pencacah
intuntuk hasil — padaN= 500 dengan data yang seluruhnya nol, jawabannya lebih dari 20 juta, masih muat diint, tetapi jarak amannya tipis.
Soal 2 — Himpunan Bagian Terdekat
Hitung dulu. N ≤ 20 berarti 2²⁰ =
1.048.576 himpunan bagian, dan menjumlahkan isi masing-masing memerlukan
sampai 20 langkah — sekitar 2 × 10⁷ operasi. Muat. Diukur pada mesin
penulis: 0,10 detik untuk N = 20.
Cara berpikir. Setiap himpunan bagian bisa diwakili
satu bilangan bulat antara 0 dan 2^N − 1. Bit
ke-i yang menyala berarti elemen ke-i ikut
dipilih. Dengan begitu "mencoba semua himpunan bagian" menjadi
perulangan biasa.
Dua hal yang perlu dibaca teliti dari soal. Pertama, himpunan bagian
kosong boleh dipilih, jadi jawaban awalnya 0 dengan jarak
S. Kedua, kalau ada seri, yang dipilih adalah total yang
lebih kecil — syarat itu harus ditulis, tidak muncul
dengan sendirinya.
Pembahasan.
long long terbaik = 0; // himpunan bagian kosong
long long selisihTerbaik = s; // |0 - s|
for (int mask = 1; mask < (1 << n); mask++) {
long long jumlah = 0;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) jumlah += a[i];
}
long long selisih = jumlah - s;
if (selisih < 0) selisih = -selisih;
if (selisih < selisihTerbaik ||
(selisih == selisihTerbaik && jumlah < terbaik)) {
selisihTerbaik = selisih;
terbaik = jumlah;
}
}Jumlah 20 elemen bernilai 10⁹ mencapai 2 × 10¹⁰, dan S
sendiri bisa 10¹⁸, jadi semuanya long long.
Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
4 10 / 3 34 4 12 |
12 |
contoh soal |
3 0 / 5 6 7 |
0 |
himpunan bagian kosong yang menang |
2 7 / 3 11 |
3 |
seri, harus pilih yang lebih kecil |
Kasus ketiga: 3 berjarak 4 dari 7, dan 11 juga berjarak 4. Jawabannya 3.
Kesalahan umum.
- Memulai
maskdari 1 tanpa menyiapkan himpunan bagian kosong sebagai jawaban awal, sehingga kasus kedua salah. - Menulis
1 << idenganisampai 31 pada tipeint— untukN≤ 20 masih aman, tetapi kebiasaan1LL << ilebih tahan saat batasannya dinaikkan. - Lupa aturan seri, sehingga jawabannya bergantung pada urutan penelusuran.
Soal 3 — Tur Terpendek
Hitung dulu. Kota pertama dikunci sebagai titik
berangkat, jadi yang dicoba adalah urutan N − 1 kota
sisanya: 7! = 5.040 urutan untuk N = 8, masing-masing
dihitung dalam 8 langkah. Sekitar 40.000 operasi — sangat ringan. Diukur
pada mesin penulis: 0,004 detik.
Mengunci kota pertama bukan sekadar mempercepat: tanpa itu, setiap
tur terhitung N kali, karena tur yang sama bisa ditulis
mulai dari kota mana saja.
Cara berpikir. next_permutation dari
<algorithm> mengubah sebuah vector menjadi urutan
berikutnya menurut abjad, dan mengembalikan false kalau
sudah urutan terakhir. Pola bakunya:
sort(v.begin(), v.end());
do {
// periksa satu urutan
} while (next_permutation(v.begin(), v.end()));Isian v di sini adalah nomor kota 1 sampai
N − 1, yang memang sudah urut naik saat dibuat, jadi
sort tidak diperlukan. Kalau data awalnya belum urut,
sort wajib — tanpa itu sebagian urutan tidak pernah
dicoba.
Pembahasan.
vector<int> urutan;
for (int i = 1; i < n; i++) urutan.push_back(i);
long long terbaik = -1;
do {
long long total = 0;
int sekarang = 0;
for (int kota : urutan) {
total += d[sekarang][kota];
sekarang = kota;
}
total += d[sekarang][0];
if (terbaik < 0 || total < terbaik) terbaik = total;
} while (next_permutation(urutan.begin(), urutan.end()));Perhatikan total += d[sekarang][0] di akhir: kurirnya
harus kembali, dan ruas terakhir itu sangat sering
terlupa.
Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
contoh soal, N = 4 |
80 |
tur 1 → 2 → 4 → 3 → 1 |
2 / 0 5 / 5 0 |
10 |
pergi dan pulang |
Kasus kedua adalah pemeriksaan ruas pulang: jawabannya 10, bukan 5.
Kesalahan umum.
- Lupa menambahkan jarak kembali ke kota pertama.
- Mencoba semua
N! urutan termasuk kota pertama. Jawabannya tetap benar, tetapi delapan kali lebih lambat dan lebih mudah salah tulis. - Memulai
terbaikdari 0, sehingga tidak pernah ada tur yang lebih kecil.
Kalau masih tersendat
- Tidak tahu apa yang harus dicoba → Bab 11, empat bentuknya: semua elemen, semua pasangan, semua himpunan bagian, semua urutan.
- Bingung pada
maskdan operasi bit → Bab 11, tabelmask↔︎ himpunan bagian dalam biner. - Brute force-mu kehabisan waktu → Bab 11, langkah 2 dan 3: hitung dulu banyaknya kemungkinan, baru tulis kodenya.
Strategi lomba yang perlu dibiasakan: tulis brute force lebih dulu, amankan poin subsoal kecilnya, baru pikirkan penyelesaian penuh.