05. Perulangan

Komputer unggul pada pekerjaan yang diulang ribuan kali tanpa bosan. Perulangan adalah alat untuk menyuruhnya melakukan itu — dan hampir semua soal OSN membutuhkannya.

for

Bentuk for dipakai ketika banyaknya pengulangan sudah diketahui:

for (int i = 1; i <= 5; i++) {
    cout << i << " ";
}
cout << "\n";
1 2 3 4 5

Tiga bagian di dalam kurung, dipisahkan titik koma:

Bagian Kapan dijalankan Contoh
awal sekali, sebelum perulangan int i = 1
syarat lanjut sebelum setiap putaran i <= 5
perubahan sesudah setiap putaran i++

Urutan pengerjaannya bukan dari kiri ke kanan begitu saja, melainkan seperti ini: awal dijalankan sekali, lalu syarat diperiksa, lalu isi dijalankan, lalu perubahan, lalu kembali memeriksa syarat, dan seterusnya. Telusuri contoh di atas:

Pemeriksaan ke- i saat diperiksa i <= 5? dicetak sesudah i++
1 1 ya 1 2
2 2 ya 2 3
3 3 ya 3 4
4 4 ya 4 5
5 5 ya 5 6
6 6 tidak — perulangan berhenti

Perhatikan dua hal yang sering mengejutkan pemula: syarat diperiksa sebelum isi dijalankan (jadi isi bisa tidak pernah dijalankan sama sekali), dan sesudah perulangan selesai nilai i sudah melewati batasnya.

Variabel i yang dibuat di dalam for (int i = ...) hanya hidup selama perulangan itu. Sesudahnya ia tidak ada lagi, dan memakainya menghasilkan 'i' was not declared in this scope.

Variasi yang sering dipakai:

for (int i = 0; i < n; i++)        // 0 sampai n-1, paling umum untuk array
for (int i = n; i >= 1; i--)       // mundur
for (int i = 0; i < n; i += 2)     // melompat dua

Banyaknya putaran bisa dihitung tanpa menjalankan programnya, dan ini yang perlu dibiasakan sejak sekarang karena Bab 10 memakainya terus-menerus:

Bentuk Banyak putaran
for (int i = 0; i < n; i++) n
for (int i = 1; i <= n; i++) n
for (int i = 0; i <= n; i++) n + 1
for (int i = 2; i <= n; i++) n - 1
for (int i = 0; i < n; i += 2) sekitar n / 2

Baris ketiga adalah kesalahan satu langkah (off-by-one) yang paling sering: mengganti < menjadi <= menambah satu putaran, dan pada array putaran tambahan itu menyentuh kotak yang tidak ada.

Di lomba, for (int i = 0; i < n; i++) jauh lebih sering dipakai daripada i = 1; i <= n. Alasannya: indeks array di C++ dimulai dari 0. Biasakan bentuk itu supaya tidak sering meleset satu langkah.

while

while dipakai ketika banyaknya pengulangan tidak diketahui sejak awal, hanya syarat berhentinya yang diketahui:

int n = 1234;
int banyak_digit = 0;

while (n > 0) {
    n /= 10;
    banyak_digit++;
}
cout << banyak_digit << "\n";   // 4

Kalau syaratnya tidak pernah menjadi salah, program berputar selamanya dan di penilai daring hasilnya Time Limit Exceeded. Pastikan ada baris di dalam perulangan yang mengubah nilai yang diperiksa — pada contoh di atas, n /= 10.

Menelusuri contoh di atas untuk n = 1234:

Pemeriksaan n n > 0? n /= 10 banyak_digit
1 1234 ya 123 1
2 123 ya 12 2
3 12 ya 1 3
4 1 ya 0 4
5 0 tidak — 4

Memilih for atau while

Kalau di soal tertulis ... Pakai
"untuk setiap dari n data" for
"ulangi k kali" for
"selama masih ada sisa", "sampai bernilai nol" while
"sampai masukan habis" while
"sampai ditemukan" while atau for + break

Keduanya sama kuat — apa pun yang bisa ditulis dengan for bisa ditulis dengan while, dan sebaliknya. Yang dipilih adalah yang membuat maksudnya paling jelas terbaca.

do ... while

Sama seperti while, tetapi isinya dijalankan minimal sekali karena syaratnya diperiksa di akhir:

int pilihan;
do {
    cin >> pilihan;
} while (pilihan < 1 || pilihan > 3);

Bentuk ini jarang dipakai untuk soal penilai daring, tetapi berguna untuk program interaktif.

break dan continue

  • break keluar dari perulangan seketika.
  • continue melompat ke putaran berikutnya.
for (int i = 1; i <= 100; i++) {
    if (i % 3 != 0) continue;   // lewati yang bukan kelipatan 3
    if (i > 20) break;          // berhenti begitu melewati 20
    cout << i << " ";
}
3 6 9 12 15 18

Buka exercises/05/02-while.cpp. Ubah angka awalnya menjadi 0, compile ulang, dan perhatikan bahwa hasil hitungan digitnya 0 — bukan 1. Ini kasus tepi yang sering terlupakan di soal.

Pola akumulasi

Pola paling sering di seluruh buku ini: siapkan sebuah penampung sebelum perulangan, ubah isinya di setiap putaran, pakai hasilnya sesudah perulangan.

long long total = 0;            // penampung
for (int i = 1; i <= n; i++) {
    total += i;                 // diubah tiap putaran
}
cout << total << "\n";          // dipakai sesudahnya

Tiga bagian itu — siapkan, ubah, pakai — selalu ada. Yang paling sering salah adalah bagian "siapkan", karena nilai awalnya berbeda untuk setiap jenis akumulasi:

Yang dihitung Nilai awal penampung Operasi di dalam perulangan
jumlah 0 total += x
hasil kali 1 hasil *= x
banyaknya yang cocok 0 banyak++
nilai terbesar elemen pertama, atau bilangan sangat kecil if (x > besar) besar = x
nilai terkecil elemen pertama, atau bilangan sangat besar if (x < kecil) kecil = x

Nilai awal 0 untuk hasil kali membuat jawabannya selalu nol; nilai awal 0 untuk "terbesar" membuat jawabannya salah bila semua data negatif. Keduanya kesalahan klasik yang lolos dari contoh kasus di soal dan baru ketahuan saat dinilai.

Buka exercises/05/03-akumulasi.cpp. Ubah long long faktorial = 1; menjadi = 0, compile ulang, dan jalankan untuk n = 10. Baris jumlah dan cacahnya tetap benar, tetapi faktorialnya menjadi 0 — satu nilai awal yang salah cukup untuk merusak satu jawaban.

Variasi yang perlu kamu hafal bentuknya:

// menghitung berapa banyak yang memenuhi syarat
int banyak = 0;
for (int i = 1; i <= n; i++) if (i % 7 == 0) banyak++;

// mencari yang terbesar
int besar = -1000000000;
for (int i = 0; i < n; i++) { cin >> x; if (x > besar) besar = x; }

// perkalian beruntun (faktorial)
long long hasil = 1;
for (int i = 2; i <= n; i++) hasil *= i;

Penampung untuk penjumlahan hampir selalu long long. Menjumlahkan 100.000 bilangan yang masing-masing sampai 10⁹ menghasilkan 10¹⁴ — int meluap jauh sebelum itu.

Memproses digit

Gabungan % 10 dan / 10 dari Bab 03 di dalam while memberi cara membaca sebuah bilangan digit demi digit, dari belakang:

long long n = 4071;
int jumlah = 0;

while (n > 0) {
    int digit = n % 10;   // ambil digit terakhir
    jumlah += digit;
    n /= 10;              // buang digit terakhir
}
cout << jumlah << "\n";   // 12

Perulangan bersarang

Perulangan boleh berada di dalam perulangan. Yang dalam berputar penuh untuk setiap putaran yang luar.

for (int baris = 1; baris <= 4; baris++) {
    for (int kolom = 1; kolom <= baris; kolom++) {
        cout << "*";
    }
    cout << "\n";
}
*
**
***
****

Cara membacanya: perulangan luar menentukan berapa baris, perulangan dalam menentukan isi satu baris. Untuk contoh di atas:

baris kolom berjalan dari–sampai bintang tercetak
1 1 … 1 1
2 1 … 2 2
3 1 … 3 3
4 1 … 4 4

Seluruhnya 1 + 2 + 3 + 4 = 10 bintang. Perhatikan juga letak cout << "\n": ia berada di dalam perulangan luar tetapi di luar perulangan dalam. Memindahkannya satu tingkat ke dalam membuat setiap bintang berpindah baris; memindahkannya ke luar membuat semuanya menjadi satu baris panjang.

Jumlah pekerjaan ikut berlipat: dua perulangan bersarang yang masing-masing sampai n mengerjakan sekitar n² langkah. Untuk n = 1000 itu sejuta langkah (cepat), untuk n = 100000 itu sepuluh miliar (jauh terlalu lambat). Bab 10 membahas cara memperkirakannya sebelum menulis kode.

Buka exercises/05/04-bersarang.cpp dan ubah agar segitiganya terbalik (baris pertama empat bintang, baris terakhir satu).

Pola banyak kasus uji

Banyak soal lomba memberi jumlah kasus uji di baris pertama. Bentuk bakunya:

int t;
cin >> t;
while (t--) {
    int a, b;
    cin >> a >> b;
    cout << a + b << "\n";
}

t-- memberikan nilai t sekarang kepada while, lalu menguranginya satu. Saat t mencapai 0, perulangan berhenti.

Pada soal banyak kasus uji, semua variabel penampung harus disiapkan ulang di dalam perulangan while (t--), bukan di luarnya. Penampung yang disiapkan sekali di luar akan membawa sisa hitungan kasus sebelumnya ke kasus berikutnya — jawaban kasus pertama benar, sisanya salah. Ini penyebab nilai sebagian yang paling sering pada soal berkasus uji banyak.

Buka exercises/05/06-banyak-kasus.cpp. Tambahkan int total = 0; sebagai baris pertama di dalam while (t--) dan ubah baris cetaknya menjadi total += a + b; cout << total << "\n";. Jalankan dengan contoh masukan di bagian atas file; keluarannya 3, 30, 14. Lalu pindahkan int total = 0; ke atas while, jalankan lagi: keluarannya berubah menjadi 3, 33, 47 karena hitungan kasus sebelumnya ikut terbawa.

Kalau perulangan tidak mau berhenti

Program yang menggantung tanpa mencetak apa-apa hampir selalu berarti satu dari tiga hal:

  1. Nilai yang diperiksa tidak pernah berubah. Periksa apakah ada baris di dalam perulangan yang benar-benar mengubahnya.
  2. Perubahannya menjauhi syarat berhenti. Misalnya i-- pada perulangan yang menunggu i mencapai n.
  3. Programnya menunggu masukan. Bukan perulangan tak berujung, melainkan cin >> yang menanti angka yang tidak pernah kamu ketik.

Hentikan program yang menggantung dengan Ctrl + C. Untuk melihat apa yang sebenarnya terjadi, sisipkan sementara cerr << i << " " << n << "\n"; di dalam perulangan — cerr dipakai, bukan cout, supaya tidak mengotori keluaran yang dinilai (Bab 10).

Pekerjaan rumah

Soal 1 — Jumlah 1 sampai N

Buka exercises/05/homework/01-jumlah-n.cpp. Baca n (sampai 1.000.000), cetak 1 + 2 + ... + n. Perhatikan tipe penampungnya.

Soal 2 — Jumlah digit

Buka exercises/05/homework/02-jumlah-digit.cpp. Baca satu bilangan bulat positif (bisa sampai 10¹⁸), cetak jumlah semua digitnya. Contoh: 4071 menghasilkan 12.

Soal 3 — Tabel perkalian

Buka exercises/05/homework/03-tabel-perkalian.cpp. Baca n, cetak tabel perkalian n × n. Setiap baris berisi n bilangan dipisahkan satu spasi, tanpa spasi tambahan di akhir baris.

Soal 4 — Segitiga angka

Buka exercises/05/homework/04-segitiga-angka.cpp. Baca n, cetak pola berikut untuk n = 4:

1
12
123
1234

Tantangan — Uji bilangan prima

Buka exercises/05/homework/05-prima.cpp. Baca satu bilangan bulat n (sampai 10¹²), cetak PRIMA atau BUKAN. Memeriksa semua bilangan dari 2 sampai n-1 terlalu lambat; berhenti di akar n, karena kalau n = a × b maka salah satu faktornya pasti tidak lebih besar dari akar n. Pakai for (long long i = 2; i * i <= n; i++). Jangan lupa n = 1 bukan prima.

Kesalahan yang paling sering

Yang ditulis Akibatnya
for (int i = 0; i <= n; i++) pada array menyentuh a[n] yang tidak ada
for (int i = 0; i < n; i++); titik koma menutup perulangan; isinya jalan sekali
penampung int untuk jumlah besar meluap menjadi negatif
penampung disiapkan di luar while (t--) kasus uji kedua dan seterusnya salah
tidak ada baris yang mengubah syarat perulangan tak berujung, Time Limit Exceeded
besar = 0 padahal data bisa negatif jawaban selalu 0
cout << "\n" salah tingkat susunan baris keluaran berantakan

Tips OSN

Pola banyak kasus uji harus otomatis. Sebagian soal OSN membungkus satu soal kecil ke dalam T kasus uji. Bentuknya selalu sama, dan sebaiknya sudah hafal:

int t;
cin >> t;
while (t--) {
    // baca satu kasus, cetak satu jawaban
}

Dua kesalahan yang sering muncul di pola ini: variabel penampung yang lupa dikembalikan ke nol di awal setiap kasus, dan jawaban yang dikumpulkan dulu lalu dicetak di akhir padahal soal meminta satu baris per kasus.

Perkirakan banyaknya langkah sebelum menulis kode. Aturan kasar yang dipakai sepanjang buku ini: mesin penilai menyelesaikan sekitar 10⁸ operasi sederhana per detik. Dari situ, batasan pada soal langsung memberi tahu bentuk penyelesaian yang muat:

Batasan N Perulangan yang masih muat dalam 1 detik
≤ 1.000 dua tingkat bersarang (N² = 10⁶)
≤ 100.000 satu tingkat, atau N log N
≤ 1.000.000 satu tingkat saja
≤ 10¹⁸ tidak boleh ada perulangan atas N

Batasan 10¹⁸ adalah isyarat yang jelas: yang diminta rumus atau pengulangan atas digit-nya, bukan atas nilainya.

Nilai antara bisa jauh lebih besar daripada masukan. Soal yang menyatakan N ≤ 10⁶ tidak menjamin semua angka di dalam programmu tetap kecil. Deret Collatz di bawah adalah contohnya: mulai dari 704.511, deretnya sempat mencapai 56.991.483.520 — melewati int lebih dari dua puluh kali lipat.

Perulangan yang tidak berhenti dinilai Time Limit Exceeded, bukan error. Kalau jawabanmu benar di komputer sendiri tetapi kena TLE di semua berkas uji, curigai syarat berhenti yang tidak pernah tercapai — misalnya while (n != 1) untuk n yang bisa bernilai 0.

Keluaran banyak baris lebih baik dikumpulkan. Untuk soal yang mencetak ratusan ribu baris, mencetak dengan endl di setiap perulangan membuat program berkali-kali lipat lebih lambat. Pakai "\n".

for (int i = 0; i < n; i++) dengan n bertipe long long bernilai 3 × 10⁹ tidak akan pernah berhenti: i bertipe int meluap menjadi negatif sebelum mencapai n. Samakan tipe pencacah dengan tipe batasnya.

Ringkasan

  • for dipakai saat banyaknya putaran diketahui, while saat hanya syarat berhentinya yang diketahui.
  • Tiga bagian for: awal (sekali), syarat (sebelum tiap putaran), perubahan (sesudah tiap putaran).
  • for (int i = 0; i < n; i++) berputar n kali dan cocok dengan indeks array; mengganti < menjadi <= menambah satu putaran.
  • Pola akumulasi: siapkan penampung, ubah tiap putaran, pakai sesudahnya. Nilai awalnya berbeda untuk jumlah, hasil kali, dan nilai terbesar.
  • while (n > 0) { n % 10; n /= 10; } membaca sebuah bilangan digit demi digit dari belakang.
  • Perulangan bersarang mengerjakan sekitar n² langkah — cukup untuk n ribuan, tidak untuk n ratusan ribu.
  • Penampung pada soal banyak kasus uji harus disiapkan ulang di setiap kasus.

Latihan bergaya OSN

Ketiga soal berikut dikerjakan dengan perulangan saja — belum perlu array. Soal ketiga memakai pola banyak kasus uji yang akan sering kamu temui di OSN.

Kerangka ketiganya ada di exercises/05/osn/.

Soal 1 — Deret Collatz

Ambil sebuah bilangan bulat positif N. Selama angkanya belum sama dengan 1, terapkan aturan berikut berulang-ulang:

  • kalau angkanya genap, bagi dua;
  • kalau angkanya ganjil, kalikan tiga lalu tambah satu.

Satu kali penerapan aturan dihitung sebagai satu langkah. Hitung berapa langkah yang diperlukan sampai angkanya menjadi 1.

Format masukan. Satu baris berisi satu bilangan bulat N.

Format keluaran. Satu baris berisi banyaknya langkah.

Batasan. 1 ≤ N ≤ 10⁶. Untuk semua N pada batasan ini, deretnya dijamin mencapai 1.

Contoh masukan

6

Contoh keluaran

8

Penjelasan. Deretnya 6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1, yaitu delapan langkah. Untuk N = 1, jawabannya 0 karena angkanya sudah 1 sejak awal.

Kerangkanya: exercises/05/osn/01-collatz.cpp

Soal 2 — Palindrom Angka

Sebuah bilangan disebut palindrom kalau urutan digitnya dibaca dari depan sama dengan dibaca dari belakang. Misalnya 12321 dan 7 adalah palindrom, sedangkan 12345 dan 10 bukan.

Format masukan. Satu baris berisi satu bilangan bulat N.

Format keluaran. Satu baris berisi YA kalau N palindrom, atau TIDAK kalau bukan.

Batasan. 1 ≤ N ≤ 10¹⁸. Bilangan tidak boleh dibaca sebagai teks.

Contoh masukan

12321

Contoh keluaran

YA

Penjelasan. Digit 12321 dibaca dari belakang tetap 12321. Perhatikan batasan 10¹⁸: bilangan sebesar itu tidak muat di int, dan perulangan sebanyak N kali jelas tidak mungkin — yang diulang adalah digitnya, dan digitnya paling banyak 19.

Kerangkanya: exercises/05/osn/02-palindrom-angka.cpp

Soal 3 — Banyak Faktor

Untuk setiap bilangan yang diberikan, hitung ada berapa bilangan bulat positif yang membaginya habis.

Format masukan. Baris pertama berisi satu bilangan bulat T, banyaknya kasus uji. T baris berikutnya masing-masing berisi satu bilangan bulat N.

Format keluaran. T baris. Baris ke-i berisi banyaknya faktor positif dari N pada kasus uji ke-i.

Batasan.

  • 1 ≤ T ≤ 50
  • 1 ≤ N ≤ 100.000

Contoh masukan

3
12
1
97

Contoh keluaran

6
1
2

Penjelasan. Faktor 12 adalah 1, 2, 3, 4, 6, dan 12 — ada enam. Bilangan 1 hanya punya satu faktor, yaitu dirinya sendiri. Bilangan 97 adalah bilangan prima, jadi faktornya hanya 1 dan 97.

Kerangkanya: exercises/05/osn/03-banyak-faktor.cpp

Bingung atau sudah selesai? Buka halaman pembahasan PR.