Portal/Notes πŸ“
Leetcode

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:

SimbolNilai
I1
V5
X10
L50
C100
D500
M1000

Aturan penting β€” subtractive notation. Angka seperti 4 tidak ditulis IIII, tapi IV (5 - 1). Begitu juga:

KombinasiNilaiLogika
IV45 - 1
IX910 - 1
XL4050 - 10
XC90100 - 10
CD400500 - 100
CM9001000 - 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:

  1. 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.
  2. Constraint 1-3999 menjamin kita tidak perlu simbol di atas MMM (3000) atau special case di atas ribuan.

Kompleksitas

AspekNilai
WaktuO(1) β€” outer loop 13 iterasi (fixed), inner while bergantung pada nilai tapi maksimum ~15 append (untuk 3888 = MMMDCCCLXXXVIII)
MemoriO(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.

Edit on GitHub

Last updated on

Integer to Roman: Greedy Mapping yang Selalu Muncul di Interview | Faisal Affan