12. Greedy — Pembahasan PR
Berkas .cpp jawabannya ada di
exercises/12/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/12/homework/solutions/01-kembalian.cpp
./jawab
Soal 1 — Uang kembalian
Soal. Lembar paling sedikit untuk membayar
k dengan pecahan rupiah.
Cara berpikir. Greedy langsung: ambil sebanyak
mungkin dari pecahan terbesar, sisanya diteruskan ke pecahan berikutnya.
k / pecahan[i] memberi jumlah lembarnya dan
k % pecahan[i] memberi sisanya — dua operasi yang sudah
dikenal sejak Bab 03.
Greedy benar di sini karena setiap pecahan rupiah habis membagi pecahan di atasnya atau berkelipatan rapi (1.000 → 2.000 → 5.000 → 10.000 → …). Sifat itu yang membuat mengambil pecahan besar tidak pernah merugikan.
Pembahasan.
int pecahan[] = {100000, 50000, 20000, 10000, 5000, 2000, 1000};
long long lembar = 0;
for (int i = 0; i < 7; i++) {
lembar += k / pecahan[i];
k %= pecahan[i];
}
cout << lembar << "\n";Uji.
| Masukan | Keluaran | Rinciannya |
|---|---|---|
1000 |
1 |
satu lembar 1.000 |
87000 |
5 |
50.000 + 20.000 + 10.000 + 5.000 + 2.000 |
1000000000 |
10000 |
sepuluh ribu lembar 100.000 |
Baris terakhir menguji tipe data: nilai k sampai 10⁹
masih muat di int, tetapi jawaban dan sisa perhitungannya
lebih aman ditampung long long.
Kesalahan umum.
- Menulis pecahan tidak terurut menurun. Greedy langsung rusak.
- Memakai
intuntukksampai 10⁹ (masih muat) tetapi lupa bahwa jumlah lembarnya bisa besar juga — untukk = 10⁹jawabannya 10.000 lembar, masih aman, tetapi biasakanlong longuntuk penampung.
Soal 2 — Penjadwalan kegiatan
Soal. Kegiatan terbanyak yang bisa diikuti.
Cara berpikir. Urutkan menurut waktu selesai, lalu ambil setiap kegiatan yang mulainya tidak lebih awal dari selesainya kegiatan terakhir yang diambil. Alasannya: kegiatan yang selesai paling awal menyisakan waktu terbanyak untuk sisanya.
Nilai awal waktu harus lebih kecil dari semua waktu
mulai yang mungkin, supaya kegiatan pertama pasti terambil.
Pembahasan.
sort(keg, keg + n, [](Kegiatan a, Kegiatan b) {
return a.selesai < b.selesai;
});
int banyak = 0, waktu = -1000000000;
for (int i = 0; i < n; i++) {
if (keg[i].mulai >= waktu) {
banyak++;
waktu = keg[i].selesai;
}
}Uji.
| Masukan | Keluaran |
|---|---|
1 / 5 5 |
1 |
3 / 0 10 / 1 2 /
3 4 |
2 |
6 / 1 4 / 3 5 /
0 6 / 5 7 / 3 8 /
8 10 |
3 |
Kasus kedua adalah contoh penyangkal untuk greedy "waktu mulai paling
awal", yang akan menjawab 1. Kalau jawabanmu 1
di sini, berarti ukuran pengurutannya masih salah. Kasus pertama menguji
kegiatan berdurasi nol — ia tetap satu kegiatan yang sah.
Kesalahan umum.
- Mengurutkan dua array terpisah (
mulai[]danselesai[]), sehingga pasangannya rusak. Pakaistruct. - Memakai
>alih-alih>=, sehingga kegiatan yang mulai tepat saat kegiatan lain selesai ikut ditolak — padahal soal mengizinkannya.
Soal 3 — Muatan perahu
Soal. Perahu paling sedikit, dua orang per perahu,
total berat ≤ k.
Cara berpikir. Orang terberat pasti harus berangkat dengan suatu perahu. Pertanyaannya hanya: siapa yang bisa menemaninya? Kalau ada yang bisa, yang paling menguntungkan adalah yang paling ringan — karena kalau yang paling ringan pun tidak muat, tidak ada yang muat.
Dari situ muncul pola dua penunjuk: kanan selalu maju
mundur satu langkah (orang terberat selalu berangkat), dan
kiri hanya maju kalau ia ikut naik.
Pembahasan.
sort(b, b + n);
int kiri = 0, kanan = n - 1, perahu = 0;
while (kiri <= kanan) {
if (b[kiri] + b[kanan] <= k) kiri++;
kanan--;
perahu++;
}Uji.
| Masukan | Keluaran | Pasangannya |
|---|---|---|
1 10 / 5 |
1 |
satu orang berangkat sendiri |
5 6 / 1 2 3 4 5 |
3 |
{1,5}, {2,4}, {3} |
5 100 / 90 20 30 70 50 |
3 |
{20,70}, {30,50}, {90} |
Perhatikan bahwa keduanya berakhir dengan satu orang berangkat sendiri. Itu wajar: yang menentukan bukan banyaknya orang, melainkan apakah yang paling ringan masih muat bersama yang paling berat.
Kesalahan umum.
- Memasangkan dua orang teringan lebih dulu. Untuk
20 30 50 70 90dengank = 100, cara itu memberi 4 perahu, bukan 3. - Menulis
while (kiri < kanan), sehingga orang terakhir yang berangkat sendirian tidak pernah terhitung.
Soal 4 — Greedy yang gagal
Soal. Cari k yang membuat greedy salah
untuk koin {1, 3, 4}.
Cara berpikir. Fungsi terbaik ditulis
rekursif: untuk setiap koin yang muat, coba ambil koin itu dan
selesaikan sisanya. Ini brute force, dan cukup cepat sampai
k = 30. (Bab 13 memperbaikinya menjadi dynamic
programming.)
Keluarannya.
k = 6: greedy 3, terbaik 2
k = 10: greedy 4, terbaik 3
k = 14: greedy 5, terbaik 4
k = 18: greedy 6, terbaik 5
...
Polanya jelas: setiap k berbentuk 4m + 2
membuat greedy salah, karena greedy mengambil koin 4 sampai tersisa 2,
lalu terpaksa memakai dua koin 1, padahal dua koin 3 lebih hemat.
Pembahasan.
int greedy(int k) {
int jumlah = 0;
for (int i = 0; i < 3; i++) { jumlah += k / koin[i]; k %= koin[i]; }
return jumlah;
}
int terbaik(int k) {
if (k == 0) return 0;
int hasil = BESAR;
for (int i = 0; i < 3; i++) {
if (k >= koin[i]) {
int calon = terbaik(k - koin[i]) + 1;
if (calon < hasil) hasil = calon;
}
}
return hasil;
}Uji. Program jawabannya mencetak setiap
k yang hasilnya berbeda:
k = 6: greedy 3, terbaik 2
k = 10: greedy 4, terbaik 3
k = 14: greedy 5, terbaik 4
k = 18: greedy 6, terbaik 5
k = 22: greedy 7, terbaik 6
k = 26: greedy 8, terbaik 7
k = 30: greedy 9, terbaik 8
Perhatikan polanya: kegagalan muncul setiap empat langkah, mulai dari 6. Yang layak direnungkan bukan polanya, melainkan kenyataan bahwa dari 30 nilai yang diuji, greedy benar pada 23 di antaranya. Penyelesaian yang benar 77 persen dari waktu tetap bernilai nol di penilai daring.
Kesalahan umum.
- Menyimpulkan "greedy selalu salah untuk soal koin". Yang benar: greedy bergantung pada himpunan koinnya.
- Menulis
terbaiktanpa kasus dasark == 0.
Tantangan — Ukuran pengurutan yang salah
Soal. Tunjukkan kegagalan greedy menurut waktu mulai dan menurut durasi.
Cara berpikir. Untuk ukuran pengurutan selain waktu selesai, penelusuran satu arah tidak cukup — setiap calon harus diperiksa terhadap semua kegiatan yang sudah diambil. Itu membuat perbandingannya adil: yang diuji adalah ukuran pengurutannya, bukan cara memeriksanya.
Pencari contoh penyangkal membangkitkan kegiatan acak dengan
n antara 2 dan 5, lalu membandingkan tiap greedy dengan
brute force atas semua 2ⁿ himpunan bagian.
Contoh penyangkal yang ditemukan.
Greedy menurut waktu mulai paling awal gagal pada lima kegiatan:
4 7 3 4 1 6 4 6 3 5
Greedy mengambil 1..6 lebih dulu, dan seluruh kegiatan
lain bentrok dengannya — hasilnya 1. Jawaban terbaik 2, misalnya
3..4 bersama 4..7.
Greedy menurut durasi terpendek gagal pada empat kegiatan:
7 10 9 12 6 10 5 9
Durasi terpendek adalah 7..10, dan setelah diambil,
semua sisanya bentrok — hasilnya 1. Jawaban terbaik 2: 5..9
bersama 9..12. Kegiatan pendek yang terletak persis di
tengah bisa menabrak dua kegiatan sekaligus.
Sementara itu, greedy menurut waktu selesai tidak pernah salah pada 20.000 percobaan — sesuai dengan buktinya.
Kesalahan umum.
- Menguji dengan
nbesar. Contoh penyangkal yang ditemukan menjadi sulit dibaca; data kecil justru lebih berguna. - Lupa menyalin ulang data sebelum memanggil greedy berikutnya. Setiap greedy mengurutkan array, sehingga urutan aslinya hilang dan perbandingannya tidak adil.
Latihan bergaya OSN — pembahasan
Berkas jawabannya ada di
exercises/12/osn/solutions/.
Soal 1 — Antrian Layanan
Cara berpikir. Perhatikan apa yang sebenarnya
dihitung. Kalau urutannya sudah ditetapkan, pelanggan pertama menunggu
0, pelanggan kedua menunggu T pelanggan pertama, pelanggan
ketiga menunggu jumlah dua yang pertama, dan seterusnya. Jadi layanan
pelanggan yang dipanggil ke-i ikut dihitung sebanyak
N − i kali di dalam total.
Dari situ jawabannya langsung terlihat: layanan yang paling lama sebaiknya dikalikan dengan angka sekecil mungkin, jadi ia dipanggil paling akhir. Urutkan naik.
Argumen penukaran. Ambil susunan terbaik mana pun.
Andaikan ada dua pelanggan berdampingan dengan urutan x
lalu y di mana x > y. Menukar keduanya
tidak mengubah waktu tunggu pelanggan lain sama sekali — jumlah layanan
sebelum mereka tetap, dan jumlah layanan setelah mereka juga tetap. Yang
berubah hanya waktu tunggu mereka berdua: semula 0 dan x,
menjadi 0 dan y, dan y < x. Jadi penukaran
selalu memperbaiki atau menyamakan. Karena setiap susunan yang tidak
urut naik pasti punya pasangan berdampingan seperti itu, susunan urut
naik pasti termasuk yang terbaik.
Pembahasan.
sort(t.begin(), t.end());
long long berjalan = 0, total = 0;
for (int i = 0; i < n; i++) {
total += berjalan; // pelanggan ke-i menunggu selama ini
berjalan += t[i];
}
cout << total << "\n";Urutan dua baris di dalam perulangan menentukan jawabannya: waktu tunggu dicatat sebelum layanan pelanggan itu sendiri ditambahkan.
Batas atas totalnya: 10.000 × 200.000 × 199.999 / 2 ≈ 2 × 10¹⁴, jadi
long long.
Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
3 / 5 1 3 |
5 |
contoh soal |
1 / 7 |
0 |
satu pelanggan, tidak menunggu |
4 / 2 2 2 2 |
12 |
0 + 2 + 4 + 6 |
Kesalahan umum.
- Menambahkan
t[i]sebelum mencatat waktu tunggu, sehingga setiap pelanggan ikut menunggu dirinya sendiri. - Mengurutkan turun.
- Memakai
intuntuk total.
Soal 2 — Titik Pemeriksaan
Cara berpikir. Godaannya adalah mengurutkan menurut ujung kiri. Itu salah: selang yang mulai lebih awal bisa jauh lebih panjang dan memberi sedikit sekali keterangan tentang di mana pos sebaiknya berdiri.
Kunci soal ini adalah ujung kanan. Urutkan selang menurut ujung kanannya. Ambil selang pertama yang belum terawasi, lalu dirikan pos tepat di ujung kanannya. Itu posisi terjauh ke kanan yang masih sah untuk selang tersebut, sehingga peluangnya mengawasi selang berikutnya paling besar. Tidak ada alasan menaruhnya lebih ke kiri.
Sesudah pos didirikan, selang berikutnya cukup diperiksa satu syarat:
apakah pos terakhir masih berada di dalamnya, yaitu apakah
titik ≥ Lᵢ. Ujung kanannya tidak perlu diperiksa, karena
urutannya menjamin ujung kanan selang berikutnya tidak lebih kecil.
Pembahasan.
vector<pair<long long, long long>> selang(n); // (kanan, kiri)
for (int i = 0; i < n; i++) {
long long l, r;
cin >> l >> r;
selang[i] = {r, l};
}
sort(selang.begin(), selang.end());
int banyak = 0;
long long titik = -1;
for (int i = 0; i < n; i++) {
long long kanan = selang[i].first;
long long kiri = selang[i].second;
if (banyak == 0 || titik < kiri) {
banyak++;
titik = kanan;
}
}Menyimpan pasangan sebagai (kanan, kiri) membuat
sort biasa langsung mengurutkan menurut ujung kanan, tanpa
perlu menulis fungsi pembanding.
Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
4 / 1 3 / 2 5 /
4 7 / 6 9 |
2 |
contoh soal |
3 / 1 10 / 2 3 /
5 6 |
2 |
selang panjang yang memuat yang lain |
2 / 1 1 / 2 2 |
2 |
selang sepanjang satu titik |
Kasus kedua adalah penguji urutan: kalau diurutkan menurut ujung
kiri, selang 1 10 diproses lebih dulu dan posnya berdiri di
kilometer 10, sehingga dua selang sisanya masing-masing perlu pos
sendiri — jawabannya menjadi 3.
Kesalahan umum.
- Mengurutkan menurut ujung kiri.
- Menempatkan pos di tengah selang, atau di ujung kirinya.
- Memakai
intuntuk koordinat sampai 10⁹ — masih muat, tetapititik = -1sebagai nilai awal harus tetap lebih kecil daripada semuaLᵢ, jadi pemeriksaanbanyak == 0lebih aman daripada bergantung pada nilai awal.
Soal 3 — Pasangan Beban
Cara berpikir. Urutkan seluruh berat, lalu pasangkan yang terkecil dengan yang terbesar, terkecil kedua dengan terbesar kedua, dan seterusnya.
Argumen penukaran. Perhatikan barang terberat, sebut
B. Ia pasti berpasangan dengan seseorang, dan beban
gerobaknya adalah B ditambah berat pasangannya. Karena
B tidak bisa dihindari, satu-satunya yang bisa dikecilkan
adalah berat pasangannya — jadi pasangan terbaik untuk B
adalah barang paling ringan. Sesudah keduanya diambil, sisanya adalah
soal yang sama dengan 2(N − 1) barang, dan alasan yang sama
berlaku lagi.
Pembahasan.
sort(a.begin(), a.end());
long long terbesar = 0;
for (int i = 0; i < n; i++) {
long long jumlah = a[i] + a[2 * n - 1 - i];
if (jumlah > terbesar) terbesar = jumlah;
}
cout << terbesar << "\n";Perulangannya hanya sampai n, bukan 2 * n:
setiap putaran sudah menangani dua barang sekaligus. Menulis
2 * n membuat setiap pasangan diperiksa dua kali —
jawabannya kebetulan tetap benar, tetapi itu tanda bahwa struktur
pasangannya belum dimengerti.
Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
3 / 3 5 2 8 1 4 |
9 |
contoh soal |
2 / 1 1 1 1 |
2 |
semua sama |
1 / 5 9 |
14 |
hanya satu gerobak |
Kesalahan umum.
- Memasangkan yang berdekatan setelah diurutkan — terkecil dengan terkecil kedua. Untuk contoh soal itu menghasilkan 13, bukan 9.
- Menjumlahkan seluruh beban lalu membaginya. Yang ditanya beban terberat, bukan rata-rata.
- Memakai
int: dua berat 10⁹ berjumlah 2 × 10⁹, sudah melewati batasint.
Kalau masih tersendat
- Tidak tahu harus mengurutkan menurut apa → Bab 12, tabel ukuran pengurutan yang lazim.
- Greedy-mu benar untuk contoh tetapi salah di penilai → Bab 12, bagian Cara meyakinkan diri bahwa greedy benar.
- Data berpasangan berantakan sesudah diurutkan → Bab 12, catatan
tentang
struct.
Aturan yang tidak boleh dilanggar: uji dengan pembanding sebelum mengirim. Greedy yang belum diuji adalah cara tercepat kehilangan seluruh nilai satu soal.