Integer to Roman: Greedy Mapping yang Selalu Muncul di Interview
Membedah soal konversi integer ke Roman numeral β dari aturan dasar numeral Romawi, jebakan subtractive notation (IV, IX, XL...), sampai solusi greedy O(1) yang bersih dan interview-ready. Lengkap dengan tabel mapping, visualisasi step-by-step, dan kenapa pendekatan ini optimal untuk constraint 1-3999.
[!TIP] Kalau kamu lihat soal yang melibatkan "konversi dengan aturan prioritas," hampir selalu greedy works β asal simbol diurutkan dari yang terbesar.
Soal
Diberikan integer num, konversi ke string Roman numeral. Roman numeral dibentuk dari tujuh simbol dasar:
| Simbol | Nilai |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
Aturan penting β subtractive notation. Angka seperti 4 tidak ditulis IIII, tapi IV (5 - 1). Begitu juga:
| Kombinasi | Nilai | Logika |
|---|---|---|
| IV | 4 | 5 - 1 |
| IX | 9 | 10 - 1 |
| XL | 40 | 50 - 10 |
| XC | 90 | 100 - 10 |
| CD | 400 | 500 - 100 |
| CM | 900 | 1000 - 100 |
Constraints: 1 <= num <= 3999
Contoh:
Input: num = 3749
Output: "MMMDCCXLIX"Input: num = 58
Output: "LVIII"Intuisi β Greedy
Roman numeral selalu dibangun dari simbol terbesar yang muat. Ini bukan kebetulan β memang sifat dasar sistem numeral Romawi.
Ambil 58: simbol terbesar yang β€ 58 adalah L (50). Sisanya 8. Simbol terbesar yang β€ 8 adalah V (5). Sisanya 3. Tiga kali I. Hasil: "LVIII".
Ambil 3749: mulai dari M (1000) tiga kali β "MMM", sisa 749. Berikutnya D (500) β "MMMD", sisa 249. Berikutnya C (100) dua kali β "MMMDCC", sisa 49. Berikutnya XL (40) β "MMMDCCXL", sisa 9. Berikutnya IX (9) β "MMMDCCXLIX".
Polanya selalu sama: ambil simbol terbesar yang β€ sisa, kurangi, ulangi.
Mapping yang Tepat
Kita perlu semua kemungkinan simbol β termasuk subtractive cases β diurutkan dari terbesar ke terkecil:
mapping := []struct {
value int
symbol string
}{
{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"},
{1, "I"},
}Tiga belas simbol. Urutan menurun. Kenapa 13? Karena kita memasukkan 6 subtractive cases ke dalam mapping sebagai simbol atomik. Ini membuat algoritma menjadi pure greedy β tidak perlu logika khusus untuk deteksi 4/9.
Solusi β Greedy O(1)
func intToRoman(num int) string {
mapping := []struct {
value int
symbol string
}{
{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"},
{1, "I"},
}
var result strings.Builder
for _, pair := range mapping {
for num >= pair.value {
result.WriteString(pair.symbol)
num -= pair.value
}
}
return result.String()
}Visualisasi
num = 3749
value=1000 (M): num=3749 β "M", num=2749 β "M", num=1749 β "M", num=749 β stop (749 < 1000)
result = ["M","M","M"], num = 749
value=900 (CM): 749 < 900 β skip
value=500 (D): 749 β₯ 500 β "D", num = 249
result = ["M","M","M","D"], num = 249
value=400 (CD): 249 < 400 β skip
value=100 (C): 249 β₯ 100 β "C", num = 149 β "C", num = 49 β stop (49 < 100)
result = ["M","M","M","D","C","C"], num = 49
value=90 (XC): 49 < 90 β skip
value=50 (L): 49 < 50 β skip
value=40 (XL): 49 β₯ 40 β "XL", num = 9
result = ["M","M","M","D","C","C","XL"], num = 9
value=10 (X): 9 < 10 β skip
value=9 (IX): 9 β₯ 9 β "IX", num = 0
result = ["M","M","M","D","C","C","XL","IX"], num = 0
Join β "MMMDCCXLIX"Kenapa Greedy Work?
Beberapa soal tidak bisa diselesaikan dengan greedy (contoh: coin change dengan denomination tertentu). Tapi soal ini selalu greedy-optimal karena:
- Simbol Romawi membentuk canonical coin system β setiap kali kita ambil simbol terbesar yang muat, kita tidak akan masuk ke situasi di mana kombinasi simbol lebih kecil bisa menghasilkan representasi yang lebih pendek.
- Constraint 1-3999 menjamin kita tidak perlu simbol di atas
MMM(3000) atau special case di atas ribuan.
Kompleksitas
| Aspek | Nilai |
|---|---|
| Waktu | O(1) β outer loop 13 iterasi (fixed), inner while bergantung pada nilai tapi maksimum ~15 append (untuk 3888 = MMMDCCCLXXXVIII) |
| Memori | O(1) β hanya list result yang panjangnya terbatas (max ~15 karakter) |
Karena constraint num β€ 3999, kompleksitasnya konstan. Untuk general case (integer berapapun), tetap O(log n) karena jumlah simbol per digit terbatas.
Alternate Approach β Divide by Place Value
Ada juga pendekatan dengan memproses per digit (ribuan/ratusan/puluhan/satuan):
func intToRoman(num int) string {
thousands := []string{"", "M", "MM", "MMM"}
hundreds := []string{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}
tens := []string{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}
ones := []string{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}
return thousands[num/1000] +
hundreds[(num%1000)/100] +
tens[(num%100)/10] +
ones[num%10]
}Ini juga O(1) dan lebih eksplisit, tapi greedy version lebih fleksibel kalau aturan berubah.
Intisari
Sort symbols descending. While the number is large enough for a symbol, append it and subtract. Repeat.
Soal ini menguji apakah kamu bisa mengenali greedy choice property β bahwa memilih simbol terbesar yang muat tidak akan membatalkan solusi optimal. Sekali kamu sadar itu, implementasinya trivial.
Last updated on