14. Modulo dan teorema-teoremanya
Operasi % sudah dipakai sejak Bab 03 untuk memeriksa
ganjil-genap. Bab ini membahasnya sebagai alat matematika:
sifat-sifatnya, cara memakainya pada bilangan raksasa, dan dua teorema
yang sering muncul di soal OSN.
Sisa bagi
a % m adalah sisa ketika a dibagi
m. Untuk a dan m positif,
hasilnya selalu di antara 0 dan m − 1.
cout << 17 % 5 << "\n"; // 2
cout << 20 % 5 << "\n"; // 0
cout << 3 % 5 << "\n"; // 3Dua bilangan disebut kongruen modulo m kalau sisanya sama:
17 ≡ 2 (mod 5), karena keduanya bersisa 2.
Cara membacanya: modulo membagi semua bilangan bulat menjadi
m kelompok. Dalam modulo 5, bilangan 2, 7, 12, 17, dan 22
berada di kelompok yang sama.
Gambaran yang paling membantu adalah jam. Jam dinding bekerja modulo 12: pukul 10 ditambah 5 jam menjadi pukul 3, bukan pukul 15, karena 15 mod 12 = 3. Semua sifat modulo di bawah ini sebenarnya sifat yang sudah kamu pakai sejak kecil saat membaca jam:
| Modulo | "Jam"-nya | Contoh |
|---|---|---|
| 12 | jam dinding | 10 + 5 ≡ 3 |
| 7 | hari dalam seminggu | 100 hari sesudah Senin = hari ke-(100 mod 7) = 2 |
| 10 | digit terakhir | 1234 mod 10 = 4 |
| 2 | ganjil atau genap | 7 mod 2 = 1 |
Untuk bilangan negatif, C++ memberi hasil yang mengikuti
tanda bilangan yang dibagi: -7 % 3 bernilai
-1, bukan 2. Dalam soal matematika, yang
dimaksud biasanya 2. Perbaikannya satu baris:
int mod_benar(int a, int m) {
return ((a % m) + m) % m;
}Sifat-sifat modulo
Inilah alasan modulo begitu berguna: ia "tembus" terhadap penjumlahan, pengurangan, dan perkalian.
| Sifat | Bentuk |
|---|---|
| penjumlahan | (a + b) mod m = ((a mod m) + (b mod m)) mod m |
| pengurangan | (a − b) mod m = ((a mod m) − (b mod m) + m) mod m |
| perkalian | (a × b) mod m = ((a mod m) × (b mod m)) mod m |
| pemangkatan | aᵇ mod m = ((a mod m)ᵇ) mod m |
Alasannya mudah dilihat untuk penjumlahan. Tulis
a = q·m + r₁ dan b = p·m + r₂. Maka:
a + b = (q + p)·m + (r₁ + r₂)
Bagian (q + p)·m habis dibagi m, jadi tidak
menyumbang apa pun ke sisanya. Yang tersisa hanya r₁ + r₂,
yaitu jumlah kedua sisa. Hal yang sama berlaku untuk perkalian, karena
semua suku hasil kali kecuali r₁ · r₂ mengandung faktor
m.
Artinya: kamu boleh mengambil sisa kapan saja di tengah perhitungan, tanpa mengubah jawaban akhir. Inilah yang membuat program bisa menjawab soal seperti "berapa 2¹⁰⁰⁰ mod 1.000.000.007" tanpa pernah menyimpan bilangan seribu digit.
Pembagian tidak punya sifat seperti itu.
(a / b) mod m bukan
((a mod m) / (b mod m)) mod m. Pembagian dalam dunia modulo
dikerjakan dengan invers modulo, dibahas di bagian akhir bab
ini.
Kenapa 1.000.000.007
Soal sering meminta jawaban "modulo 10⁹ + 7". Alasannya:
- Jawaban sebenarnya terlalu besar untuk disimpan (bisa ribuan digit).
- 10⁹ + 7 adalah bilangan prima, yang membuat pembagian modulo bisa dikerjakan dengan teorema Fermat.
- Hasil kali dua bilangan di bawah 10⁹ + 7 masih muat di
long long(sekitar 10¹⁸ < 9,2 × 10¹⁸).
Pola penulisannya:
const long long MOD = 1000000007;
long long hasil = 1;
for (int i = 1; i <= n; i++) {
hasil = hasil * i % MOD; // ambil sisa setiap kali
}Telusuri untuk n = 5 dengan MOD = 7, supaya
angkanya kecil dan mudah diperiksa:
i |
hasil * i |
% 7 |
hasil sesudahnya |
|---|---|---|---|
| 1 | 1 × 1 = 1 | 1 | 1 |
| 2 | 1 × 2 = 2 | 2 | 2 |
| 3 | 2 × 3 = 6 | 6 | 6 |
| 4 | 6 × 4 = 24 | 3 | 3 |
| 5 | 3 × 5 = 15 | 1 | 1 |
Periksa: 5! = 120, dan 120 = 17 × 7 + 1, jadi 120 mod 7 memang 1.
Perhatikan bahwa nilai antaranya tidak pernah melebihi 24 walaupun
jawaban sesungguhnya 120 — dan pada soal sungguhan, itulah bedanya
antara bilangan yang muat di long long dan bilangan sejuta
digit.
hasil * i harus bertipe long long. Kalau
hasil bertipe int, perkaliannya dihitung
sebagai int dan meluap sebelum sempat di-modulo — jawaban
salah tanpa pesan apa pun.
Buka
exercises/14/01-sifat-modulo.cpp.
Program itu menghitung 123456789 × 987654321 mod 1000000007
dengan dua cara: langsung dan dengan mengambil sisa lebih dulu. Ubah
tipe long long menjadi int pada salah satunya
dan lihat jawabannya rusak.
Pangkat modulo cepat
Menghitung aᵇ mod m dengan perulangan b kali memerlukan
O(b) langkah — mustahil untuk b = 10¹⁸. Caranya dipercepat
dengan membelah pangkat:
- kalau
bgenap: aᵇ = (aᵇ⁄²)² - kalau
bganjil: aᵇ = a × aᵇ⁻¹
Setiap langkah membagi b dua, jadi jumlah langkahnya
O(log b). Untuk b = 10¹⁸ itu sekitar 60 langkah.
Cara kerjanya bisa dibaca dari bentuk biner pangkatnya. Karena 5 = 101 dalam biner, artinya 5 = 4 + 1, sehingga:
a⁵ = a⁴ · a¹
Perulangan di bawah ini memeriksa bit b satu per satu
dari kanan. Variabel a dikuadratkan terus sehingga
berturut-turut bernilai a¹, a², a⁴, a⁸, …, dan hanya dikalikan ke
hasil saat bit yang bersangkutan menyala.
long long pangkat_mod(long long a, long long b, long long m) {
long long hasil = 1;
a %= m;
while (b > 0) {
if (b % 2 == 1) hasil = hasil * a % m;
a = a * a % m;
b /= 2;
}
return hasil;
}Telusuri pangkat_mod(3, 5, 1000):
b |
b ganjil? |
hasil |
a |
|---|---|---|---|
| 5 | ya | 1 × 3 = 3 | 9 |
| 2 | tidak | 3 | 81 |
| 1 | ya | 3 × 81 = 243 | 6561 mod 1000 = 561 |
| 0 | — | 243 | — |
Benar: 3⁵ = 243.
Bacanya bersama bentuk biner tadi: bit yang menyala pada 5 = 101₂
adalah bit ke-0 dan bit ke-2, sehingga yang dikalikan ke
hasil adalah a¹ = 3 dan a⁴ = 81. Hasilnya 3 × 81 = 243.
Buka
exercises/14/02-pangkat-mod.cpp.
Sisipkan
cerr << "b=" << b << " hasil=" << hasil << " a=" << a << "\n";
sebagai baris pertama di dalam while, lalu
jalankan untuk 3 5 1000. Setiap baris memperlihatkan isi
ketiga variabel sebelum langkah itu dikerjakan; cocokkan dengan kolom
b dan hasil pada tabel di atas.
Teorema Fermat kecil
Kalau
pbilangan prima danatidak habis dibagip, maka aᵖ⁻¹ ≡ 1 (mod p).
Contoh: p = 7, a = 3 → 3⁶ = 729 = 104 × 7 + 1, jadi 729 ≡ 1 (mod 7).
Gunanya di OSN ada dua.
1. Memendekkan pangkat raksasa. Karena aᵖ⁻¹ ≡ 1,
pangkat bisa diambil modulo p − 1:
a^b mod p = a^(b mod (p−1)) mod p, asalkan
atidak habis dibagip.
Syarat "a tidak habis dibagi p" itu wajib.
Untuk p = 7 dan a = 7, 7⁶ mod 7 bernilai 0,
bukan 1. Pada soal kombinasi (Bab 17), syarat ini otomatis terpenuhi
karena MOD = 10⁹ + 7 jauh lebih besar daripada seluruh
faktorial yang dipakai — tetapi begitu p kecil, syaratnya
harus diperiksa.
2. Pembagian modulo. Kalikan kedua sisi dengan a⁻¹:
a⁻¹ ≡ aᵖ⁻² (mod p)
Jadi membagi dengan a dalam dunia modulo p
sama dengan mengalikan dengan pangkat_mod(a, p - 2, p).
long long invers(long long a, long long p) {
return pangkat_mod(a, p - 2, p); // hanya kalau p prima
}
// (x / y) mod p
long long bagi_mod(long long x, long long y, long long p) {
return x % p * invers(y, p) % p;
}Ini yang membuat rumus kombinasi (Bab 17) bisa dihitung modulo 10⁹ + 7, walaupun rumusnya berisi pembagian faktorial.
Buka
exercises/14/03-fermat.cpp.
Program itu memeriksa aᵖ⁻¹ mod p untuk banyak nilai a
dengan p = 7 dan p = 1.000.000.007, lalu
memakai invers untuk membagi. Jalankan, lalu ganti p
menjadi bilangan bukan prima (misalnya 12) dan lihat teoremanya
gagal.
Pola soal yang memakai modulo
Digit terakhir. Digit satuan suatu bilangan adalah
nilainya mod 10. Digit terakhir 7¹⁰⁰ adalah
pangkat_mod(7, 100, 10).
Siklus dan jam. Hari ke-n dari hari
Senin adalah n % 7. Jarum jam setelah n jam
adalah (sekarang + n) % 12.
Uji habis dibagi. Sebuah bilangan habis dibagi 3 (atau 9) kalau jumlah digitnya habis dibagi 3 (atau 9). Alasannya: 10 ≡ 1 (mod 9), jadi setiap digit menyumbang nilainya sendiri.
Menghindari luapan. Soal yang jawabannya besar hampir selalu meminta hasilnya modulo suatu bilangan, justru supaya bisa dihitung.
Pekerjaan rumah
Soal 1 — Modulo yang benar
Buka
exercises/14/homework/01-mod-benar.cpp.
Baca a dan m (a bisa negatif,
m positif), cetak sisa bagi yang selalu berada di antara
0 dan m − 1.
Soal 2 — Faktorial modulo
Buka
exercises/14/homework/02-faktorial-mod.cpp.
Baca n (≤ 1.000.000), cetak
n! mod 1.000.000.007.
Soal 3 — Pangkat modulo cepat
Buka
exercises/14/homework/03-pangkat-mod.cpp.
Baca a, b, dan m (b
bisa sampai 10¹⁸), cetak aᵇ mod m. Tulis fungsi
pangkat_mod dan uji dengan 2 10 1000
(jawabannya 24) dan 3 0 7 (jawabannya 1).
Soal 4 — Uji habis dibagi 9
Buka
exercises/14/homework/04-habis-dibagi.cpp.
Baca sebuah bilangan sangat panjang sebagai string (sampai
100.000 digit), cetak YA atau TIDAK apakah ia
habis dibagi 9. Bilangannya tidak muat di long long, jadi
pakai sifat jumlah digit.
Tantangan — Invers modulo
Buka
exercises/14/homework/05-invers.cpp.
Baca x, y, dan p prima. Cetak
(x / y) mod p memakai teorema Fermat kecil. Periksa
jawabanmu: kalikan kembali hasilnya dengan y modulo
p — harus kembali menjadi x mod p. Cetak juga
hasil pemeriksaan itu.
Kesalahan yang paling sering
| Yang ditulis | Akibatnya |
|---|---|
int hasil untuk perkalian modulo |
meluap sebelum sempat di-modulo |
(a - b) % m dengan a < b |
hasil negatif; tulis ((a - b) % m + m) % m |
% dipakai pada bilangan negatif tanpa perbaikan |
jawaban di luar 0 … m−1 |
(a / b) % m dengan pembagian biasa |
sama sekali tidak berhubungan dengan jawaban; pakai invers |
pangkat_mod tanpa a %= m di awal |
a * a bisa meluap bila a sudah besar |
teorema Fermat dipakai saat p bukan prima |
invers yang dihasilkan salah |
lupa mengambil % MOD di satu tempat |
jawaban benar untuk data kecil, meluap untuk data besar |
Tips OSN
Kalimat "keluarkan jawabannya modulo 1.000.000.007" berarti jawabannya sangat besar. Kalimat itu sekaligus petunjuk bahwa yang diminta adalah menghitung, bukan menyusun: soal yang jawabannya perlu disisakan hampir selalu soal kombinatorika atau DP dengan jumlah cara yang meledak.
Ambil sisa di setiap langkah, bukan di akhir.
Menunda % MOD sampai baris terakhir membuat penampungnya
meluap jauh sebelum sampai ke sana. Aturan praktisnya: setiap kali ada
+ atau * pada nilai yang sudah bermodulo,
langsung ikuti dengan % MOD.
Perkalian dua sisa memerlukan
long long. Dua bilangan di bawah 10⁹ + 7 yang
dikalikan mencapai sekitar 10¹⁸ — melewati int lebih dari
empat ratus juta kali lipat, dan masih muat di long long.
Inilah alasan modulusnya dipilih sebesar itu tetapi tidak lebih.
% di C++ bisa memberi hasil negatif.
-7 % 3 bernilai -1, bukan 2.
Kalau ada pengurangan atau data negatif, bungkus dengan pola baku:
hasil = ((a - b) % MOD + MOD) % MOD;Kesalahan ini tidak terlihat pada contoh soal yang seluruhnya positif, dan baru muncul sebagai Wrong Answer pada berkas uji.
Pembagian tidak ada di dunia modulo.
(a / b) % MOD hampir selalu salah. Yang benar adalah
mengalikan dengan invers modulo, dan untuk modulus prima inversnya
dihitung dengan pangkat(b, MOD - 2). Setiap kali muncul
pembagian pada rumus — misalnya pada rumus kombinasi — pikirkan
invers.
Eksponen yang terlalu besar disisakan terhadap
MOD − 1. Teorema Fermat kecil memberi
a^(p−1) ≡ 1 (mod p) untuk p prima dan
a bukan kelipatan p. Perhatikan modulus yang
berbeda: alasnya bermodulo p, eksponennya bermodulo
p − 1. Tertukar adalah kesalahan yang sangat sering
terjadi.
Pangkat modulo cepat memerlukan sekitar 30 putaran untuk eksponen 10⁹. Kalau ia dipanggil di dalam perulangan sepanjang 10⁶, biayanya menjadi 3 × 10⁷ — masih muat. Tetapi kalau dipanggil di dalam perulangan bersarang, hitung ulang sebelum mengetik.
Ringkasan
a % madalah sisa bagi; untukadanmpositif hasilnya 0 sampaim − 1. Untukanegatif, C++ memberi hasil negatif — perbaiki dengan((a % m) + m) % m.- Modulo "tembus" terhadap penjumlahan, pengurangan, dan perkalian, jadi sisa boleh diambil kapan saja di tengah perhitungan.
- Modulo tidak tembus terhadap pembagian; pembagian dikerjakan dengan invers modulo.
- 10⁹ + 7 dipilih karena prima dan karena hasil kali dua bilangan di
bawahnya masih muat di
long long. - Pangkat modulo cepat memerlukan O(log b) langkah dengan membelah pangkat menjadi bentuk binernya.
- Teorema Fermat kecil: untuk
pprima danatidak habis dibagip, aᵖ⁻¹ ≡ 1 (mod p). Akibatnya inversaadalahpangkat_mod(a, p - 2, p).
Latihan bergaya OSN
Ketiga soal berikut memakai tiga alat berbeda dari bab ini: pangkat modulo cepat, teorema Fermat kecil, dan sifat sisa pada jumlah awalan.
Kerangka ketiganya ada di
exercises/14/osn/.
Soal 1 — Sisa Deret Pangkat
Hitung 1^K + 2^K + … +
N^K, lalu keluarkan sisanya jika dibagi
1.000.000.007.
Format masukan. Satu baris berisi dua bilangan bulat
N dan K.
Format keluaran. Satu baris berisi sisa hasil penjumlahan itu.
Batasan.
- 1 ≤
N≤ 10⁶ - 1 ≤
K≤ 10⁹
Contoh masukan
3 2
Contoh keluaran
14
Penjelasan. 1² + 2² + 3² = 1 + 4 + 9 = 14, dan 14
lebih kecil daripada modulusnya sehingga sisanya tetap 14. Perhatikan
besarnya K: menghitung i^K dengan
mengalikan K kali jelas tidak mungkin.
Kerangkanya:
exercises/14/osn/01-deret-pangkat.cpp
Soal 2 — Pangkat Bertingkat
Hitung A pangkat (B pangkat
C), lalu keluarkan sisanya jika dibagi 1.000.000.007.
Format masukan. Satu baris berisi tiga bilangan
bulat A, B, dan C.
Format keluaran. Satu baris berisi sisanya.
Batasan. 1 ≤ A, B,
C ≤ 10⁹. Perhatikan bahwa A selalu lebih kecil
daripada 1.000.000.007, jadi A tidak pernah merupakan
kelipatan modulusnya.
Contoh masukan
2 3 2
Contoh keluaran
512
Penjelasan. B^C = 3² = 9,
dan 2⁹ = 512. Untuk masukan yang lebih besar,
B^C sendiri tidak bisa disimpan di tipe apa
pun — misalnya 10⁹ pangkat 10⁹ punya sekitar sembilan miliar digit. Jadi
eksponennya harus diperkecil lebih dulu, dan syarat pada A
di atas adalah yang membuatnya sah.
Kerangkanya:
exercises/14/osn/02-pangkat-bertingkat.cpp
Soal 3 — Potongan Habis Dibagi K
Diberikan barisan N bilangan bulat. Hitung ada berapa
potongan berurutan yang tidak kosong dan jumlah isinya habis dibagi
K.
Format masukan. Baris pertama berisi dua bilangan
bulat N dan K. Baris kedua berisi
N bilangan bulat.
Format keluaran. Satu baris berisi banyaknya potongan.
Batasan.
- 1 ≤
N≤ 200.000 - 1 ≤
K≤ 10⁶ - −10⁹ ≤
Aᵢ≤ 10⁹
Contoh masukan
5 3
1 2 3 4 1
Contoh keluaran
4
Penjelasan. Potongannya adalah 2 3 4
(jumlah 9), 3 (jumlah 3), 1 2 3 (jumlah 6),
dan 1 2 (jumlah 3). Bilangan negatif juga mungkin muncul
pada berkas uji, dan % di C++ memberi sisa negatif untuk
bilangan negatif — itu bagian dari soalnya.
Kerangkanya:
exercises/14/osn/03-subarray-habis-dibagi.cpp
Bingung atau sudah selesai? Buka halaman pembahasan PR.