06. Fungsi — Pembahasan PR
Berkas .cpp jawabannya ada di
exercises/06/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/06/homework/solutions/01-maksimum.cpp
./jawab
Soal 1 — Fungsi maksimum
Soal. Tulis maksimum(a, b) lalu
maksimum3(a, b, c) yang memakainya.
Cara berpikir. Inti soal ini: fungsi boleh memanggil
fungsi lain. Setelah maksimum untuk dua bilangan ada,
bandingkan hasilnya dengan bilangan ketiga — tidak perlu menulis ulang
logikanya.
Pembahasan.
int maksimum(int a, int b) {
return (a > b) ? a : b;
}
int maksimum3(int a, int b, int c) {
return maksimum(maksimum(a, b), c);
}Uji.
| Masukan | Keluaran |
|---|---|
3 9 5 |
9 |
7 7 7 |
7 |
-5 -2 -9 |
-2 |
Nilai soal ini bukan pada jawabannya, melainkan pada bentuknya:
maksimum3 memanggil maksimum dua kali, bukan
menulis ulang perbandingannya. Pola "fungsi kecil dipakai fungsi yang
lebih besar" inilah yang membuat soal panjang tetap terkendali.
Kesalahan umum.
- Menyalin logika perbandingan ke dalam
maksimum3alih-alih memanggilmaksimum. Jawaban tetap benar, tetapi kesempatan belajarnya hilang. - Lupa
returndi salah satu cabang. Compiler hanya memberi peringatan (dengan-Wall), dan nilai kembalinya menjadi sampah.
Soal 2 — Cacah bilangan prima
Soal. Hitung banyaknya bilangan prima dari 1 sampai
n.
Cara berpikir. Pisahkan dua pekerjaan: satu fungsi
menjawab "apakah satu bilangan prima", main mengurus
perulangan dan pencacahan. Fungsi prima sama persis dengan
tantangan Bab 05, kini dibungkus agar bisa dipakai ulang.
Pola return false di tengah perulangan menggantikan
variabel penanda dan break — begitu satu faktor ditemukan,
jawabannya sudah pasti.
Pembahasan.
bool prima(long long n) {
if (n < 2) return false;
for (long long i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}
int banyak = 0;
for (int i = 2; i <= n; i++) if (prima(i)) banyak++;
cout << banyak << "\n";Perlu diingat batasnya: cara ini memeriksa setiap bilangan satu per
satu, dengan biaya sekitar n √n langkah. Untuk
n = 1.000.000 masih jalan beberapa detik; di atas itu
dibutuhkan saringan Eratosthenes, yang memakai array dan
dibahas di Bab 07.
Uji.
| Masukan | Keluaran | Catatan |
|---|---|---|
1 |
0 |
1 bukan bilangan prima |
2 |
1 |
bilangan prima terkecil |
10 |
4 |
2, 3, 5, 7 |
100 |
25 |
angka yang disebut di soal |
Baris pertama adalah kasus tepi yang wajib: fungsi
prima(1) harus mengembalikan false. Kalau
pemeriksaannya hanya berupa perulangan i * i <= n,
perulangan itu tidak pernah berjalan untuk n = 1 dan
fungsinya salah menjawab true.
Kesalahan umum.
- Memulai perulangan dari 1, lalu ikut menghitung 1 sebagai prima.
Penjaga
if (n < 2) return false;mencegahnya. - Menghitung 2 sebagai bukan prima karena perulangan
i * i <= nditulisi < n.
Soal 3 — Pangkat rekursif
Soal. pangkat(a, b) secara
rekursif.
Cara berpikir. Rumusnya langsung menjadi kode: aᵇ =
a × aᵇ⁻¹, dan a⁰ = 1. Kasus dasar b == 0 harus diperiksa
lebih dulu, jika tidak rekursinya turun ke b negatif dan
tidak pernah berhenti.
Pembahasan.
long long pangkat(long long a, int b) {
if (b == 0) return 1;
return a * pangkat(a, b - 1);
}Cara ini memerlukan b pemanggilan. Ada cara yang jauh
lebih cepat dengan membelah pangkatnya menjadi dua (a⁸ = a⁴ × a⁴), hanya
butuh sekitar log₂ b langkah. Itulah pangkat modulo cepat di
Bab 14.
Uji.
| Masukan | Keluaran |
|---|---|
2 10 |
1024 |
5 0 |
1 |
2 60 |
1152921504606846976 |
Baris kedua menguji kasus dasarnya: apa pun a, pangkat
nol bernilai 1. Baris ketiga menguji batas yang disebut soal — 2⁶⁰ masih
muat di long long, sedangkan 2⁶⁴ sudah tidak.
Kesalahan umum.
- Menulis kasus dasar
b == 1, sehinggapangkat(a, 0)tidak pernah berhenti. - Memakai
intuntuk hasil. 2⁴⁰ sudah melewati batasint.
Soal 4 — Membalik bilangan
Soal. Balik urutan digit, lalu periksa palindrom.
Cara berpikir. Membangun bilangan terbalik memakai satu baris yang perlu dihafal:
hasil = hasil * 10 + n % 10;Setiap putaran menggeser hasil satu digit ke kiri (dikali 10) lalu
menempelkan digit terakhir n di posisi satuan. Contoh untuk
4071:
| Putaran | n |
digit | hasil |
|---|---|---|---|
| awal | 4071 | — | 0 |
| 1 | 407 | 1 | 1 |
| 2 | 40 | 7 | 17 |
| 3 | 4 | 0 | 170 |
| 4 | 0 | 4 | 1704 |
Setelah fungsinya ada, pemeriksaan palindrom cukup satu baris:
balik(n) == n.
Pembahasan.
long long balik(long long n) {
long long hasil = 0;
while (n > 0) {
hasil = hasil * 10 + n % 10;
n /= 10;
}
return hasil;
}
if (balik(n) == n) cout << "YA\n";
else cout << "TIDAK\n";Uji.
| Masukan | balik(n) |
Keluaran |
|---|---|---|
1221 |
1221 | YA |
7 |
7 | YA |
4071 |
1704 | TIDAK |
1200 |
21 | TIDAK |
Baris terakhir memperlihatkan kenapa membalik bilangan
bukan operasi yang bisa dibalik lagi: 1200
dibalik menjadi 0021, dan nol di depan hilang sendirinya
sehingga tersisa 21. Untuk memeriksa palindrom hal itu
tidak masalah, tetapi kalau soal meminta bilangan terbalik dicetak apa
adanya, kasus angka nol di belakang perlu diperhatikan.
Kesalahan umum.
- Menulis
hasil = hasil + n % 10tanpa mengalikan 10, sehingga yang dihasilkan jumlah digit, bukan bilangan terbalik. - Mengubah
ndimaindengan memanggil fungsi, lalu membandingkan dengannyang sudah berubah. Karena parameternya salinan,ndimainaman — ini contoh gunanya pengiriman salinan.
Tantangan — Fibonacci dengan ingatan
Soal. Percepat fib rekursif dengan
menyimpan hasil yang sudah dihitung.
Cara berpikir. Versi lambat menghitung
fib(40) dengan lebih dari 300 juta pemanggilan, padahal
jawaban yang berbeda hanya ada 41 buah. Semuanya terhitung ulang
berkali-kali.
Perbaikannya tiga baris: sediakan array ingat berisi
-1 sebagai tanda "belum pernah dihitung", periksa array itu
di awal fungsi, dan isi array itu sebelum mengembalikan hasil.
Pembahasan.
long long ingat[100];
long long fib(int n) {
if (n <= 1) return n;
if (ingat[n] != -1) return ingat[n];
ingat[n] = fib(n - 1) + fib(n - 2);
return ingat[n];
}
int main() {
for (int i = 0; i < 100; i++) ingat[i] = -1;
...
}Sesudah perubahan, setiap nilai dihitung tepat sekali:
fib(45) selesai seketika, dan fib(90) pun
masih muat di long long (2.880.067.194.370.816.120).
Uji.
| Masukan | Keluaran |
|---|---|
0 |
0 |
10 |
55 |
45 |
1134903170 |
90 |
2880067194370816120 |
Bandingkan waktunya: fib(45) tanpa ingatan memerlukan
3.672.623.805 pemanggilan, dengan ingatan hanya 46 keadaan yang
masing-masing dihitung sekali — jawabannya muncul seketika. Baris
terakhir adalah batas praktis tipe long long:
fib(93) sudah melewatinya.
Kesalahan umum.
- Lupa mengisi
ingatdengan-1. Array global berisi nol, dan nol adalah jawaban sah untukfib(0)— akibatnya semua jawaban menjadi nol. - Memakai
0sebagai tanda "belum dihitung" pada soal yang jawabannya bisa bernilai nol. Pilih tanda yang mustahil menjadi jawaban. - Menyimpan hasil tetapi tidak memeriksanya di awal fungsi, sehingga tidak ada yang lebih cepat.
Latihan bergaya OSN — pembahasan
Berkas jawabannya ada di
exercises/06/osn/solutions/.
Soal 1 — Akar Digit
Cara berpikir. Soal ini punya dua tingkat perulangan yang berbeda sifatnya: satu menjumlahkan digit, satu lagi mengulangi penjumlahan itu sampai hasilnya tinggal satu digit. Memisahkan yang pertama menjadi fungsi membuat yang kedua tinggal satu baris.
Perulangan luarnya berhenti saat n < 10. Jangan
memakai "ulangi dua kali" atau jumlah putaran tetap: untuk
N sampai 10¹⁸, jumlah digitnya paling besar 19 × 9 = 171,
lalu 1 + 7 + 1 = 9 — jadi paling banyak tiga putaran, tetapi
menuliskannya sebagai syarat jauh lebih aman daripada menghitungnya
sendiri.
Pembahasan.
long long jumlahDigit(long long n) {
long long total = 0;
while (n > 0) {
total += n % 10;
n /= 10;
}
return total;
}
int main() {
int t;
cin >> t;
while (t--) {
long long n;
cin >> n;
while (n >= 10) n = jumlahDigit(n);
cout << n << "\n";
}
return 0;
}Ada jalan pintas yang tidak memerlukan perulangan sama sekali: akar
digit sebuah bilangan positif selalu sama dengan
1 + (n - 1) % 9. Sifat itu berasal dari kenyataan bahwa
jumlah digit dan bilangan aslinya selalu bersisa sama saat dibagi 9 —
alasannya dibahas di Bab 14. Kirim versi perulangan lebih dulu, lalu
bandingkan keduanya untuk semua n dari 1 sampai 100000
sebagai latihan.
Uji.
| Masukan | Keluaran |
|---|---|
9875 |
2 |
9 |
9 |
999999999999999999 |
9 |
1000000000000000000 |
1 |
Kesalahan umum.
- Memakai
intuntukn, sehingga batas atas tidak terbaca. - Menulis
while (n > 10), sehingganyang bernilai tepat 10 tidak pernah disederhanakan dan jawabannya tercetak10— dua digit. - Menjumlahkan digit hanya satu kali, sehingga
9875menjawab29.
Soal 2 — Menara Hanoi
Cara berpikir. Ini soal rekursi yang paling murni: penyelesaiannya didefinisikan dengan penyelesaian soal yang sama berukuran lebih kecil.
Untuk memindahkan n cakram dari asal ke
tujuan, cakram terbesar baru bisa bergerak kalau
n − 1 cakram di atasnya sudah menyingkir ke tiang ketiga.
Jadi urutannya selalu tiga langkah:
- pindahkan
n − 1cakram dariasalkebantu, memakaitujuansebagai tiang bantu; - pindahkan satu cakram terbesar dari
asalketujuan; - pindahkan
n − 1cakram daribantuketujuan, memakaiasalsebagai tiang bantu.
Kasus dasarnya n == 0: tidak ada yang perlu
dipindahkan.
Perhatikan bahwa peran ketiga tiang bertukar di setiap pemanggilan. Itulah sebabnya ketiganya dikirim sebagai parameter, bukan ditulis tetap.
Pembahasan.
void pindah(int n, char asal, char bantu, char tujuan) {
if (n == 0) return;
pindah(n - 1, asal, tujuan, bantu);
cout << asal << " " << tujuan << "\n";
pindah(n - 1, bantu, asal, tujuan);
}
int main() {
int n;
cin >> n;
long long banyak = (1LL << n) - 1;
cout << banyak << "\n";
pindah(n, 'A', 'B', 'C');
return 0;
}1LL << n adalah cara cepat menulis
2^n. Untuk n ≤ 15 sebenarnya int
sudah cukup, tetapi kebiasaan memakai 1LL menghindarkan
luapan saat batasannya dinaikkan.
Uji. Untuk N = 3, keluarannya:
7
A C
A B
C B
A C
B A
B C
A C
Periksa dua hal: banyaknya baris perpindahan harus tepat
2^N − 1, dan cakram terbesar hanya boleh bergerak satu
kali, tepat di tengah daftar. Untuk N = 15, keluarannya
32.768 baris — satu baris jumlah ditambah 32.767 perpindahan.
echo 15 | ./hanoi | wc -l
Kesalahan umum.
- Menukar urutan parameter pada salah satu pemanggilan rekursif.
Programnya tetap jalan dan tetap mencetak 2^
N− 1 baris, tetapi urutannya melanggar aturan cakram. - Memakai
endldi dalam rekursi. UntukN= 15 itu 32.767 kali flush yang tidak perlu. - Menulis kasus dasar
if (n == 1)lalu lupa mencetak perpindahannya.
Soal 3 — Bilangan Sempurna
Cara berpikir. Bagian yang diulang tiga kali —
menjumlahkan faktor — diangkat menjadi fungsi, sehingga
main tinggal membandingkan. Cara mencari faktornya sama
seperti soal Bab 05: berpasangan, sampai akar N.
Dua hal yang berbeda dari soal Bab 05. Pertama, yang dijumlahkan
adalah faktor sejati, jadi N sendiri tidak
ikut. Karena itu pencacahan dimulai dari total = 1 dan
perulangannya dari i = 2. Kedua, N bisa
mencapai 10⁹, sehingga jumlah faktornya bisa melewati batas
int — penampungnya long long.
Bilangan 1 perlu ditangani terpisah: ia tidak punya faktor sejati
sama sekali, sedangkan rumus total = 1 akan menjawab 1.
Pembahasan.
long long jumlahFaktorSejati(int n) {
if (n == 1) return 0;
long long total = 1; // 1 selalu faktor sejati
for (int i = 2; (long long)i * i <= n; i++) {
if (n % i == 0) {
total += i;
int pasangan = n / i;
if (pasangan != i) total += pasangan;
}
}
return total;
}Uji.
| Masukan | Keluaran | Yang diperiksa |
|---|---|---|
6 |
SEMPURNA |
bilangan sempurna terkecil |
28 |
SEMPURNA |
bilangan sempurna berikutnya |
12 |
BERLEBIH |
1+2+3+4+6 = 16 |
8 |
KURANG |
1+2+4 = 7 |
1 |
KURANG |
kasus tepi tanpa faktor sejati |
999999999 |
KURANG |
batas atas, uji kecepatan dan tipe |
Kesalahan umum.
- Memulai perulangan dari
i = 1dan menambahkan pasangannya, sehinggaNsendiri ikut terjumlah dan semua bilangan menjadiBERLEBIH. - Lupa memisahkan
N= 1, sehingga jawabannyaSEMPURNA. - Memeriksa
isampaiN, bukan sampai akarN. UntukT= 100 danN= 10⁹ itu 10¹¹ putaran — jauh melewati batas waktu. - Lupa pemeriksaan
pasangan != ipada bilangan kuadrat.
Kalau masih tersendat
- Fungsi tidak dikenali saat dipanggil → Bab 06, bagian Bentuk sebuah fungsi (tulis di atas pemanggilnya, atau deklarasikan lebih dulu).
- Perubahan di dalam fungsi tidak terbawa keluar → Bab 06, tabel pada bagian Nilai salinan dan referensi.
- Rekursi berhenti mendadak atau tidak berhenti → Bab 06, bagian Rekursi dan gambar tumpukan pemanggilannya.
Sebelum lanjut ke Bab 07, pastikan kamu bisa menjelaskan dalam satu
kalimat kenapa fib rekursif polos lambat — jawabannya
adalah setengah dari isi Bab 13.