Faisalaffan Blog
|
🇮🇩
Faisalaffan Blog

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.

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

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:

Plaintext
Input:  num = 3749
Output: "MMMDCCXLIX"
Plaintext
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:

Go
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)

Go
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

Plaintext
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

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):

Go
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.

Artikel terkait

Rekayasa Perangkat Lunak

Search & Catalog 15k SKU Q-Commerce: Relevan + In-Stock Real-time

System design search dan catalog untuk quick commerce dengan 15.000+ SKU. Elasticsearch full-text search dengan multi-match query dan typo tolerance (fuzziness), filter ketersediaan per-hub via Redis pipeline query-time filtering, ranking berbasis relevansi, ketersediaan, margin, dan popularitas, index update via Kafka streaming, dan autocomplete handler. Implementasi Golang lengkap dengan worker concurrency pattern untuk index sync.
Faisal Affan
Faisal Affan
Diterbitkan

Real-time Order Tracking Q-Commerce: Driver Bergerak di Peta Live

System design real-time order tracking untuk quick commerce. Driver GPS ingestion via WebSocket dengan throttling dan validasi, Kalman filter untuk position smoothing, Redis pub/sub fan-out by order ID untuk scale ribuan driver concurrent, dan Server-Sent Events (SSE) push ke user client. Implementasi Golang dengan pattern WebSocket goroutine pool, pub/sub broker, dan concurrent connection management.
Faisal Affan
Faisal Affan
Diterbitkan
Rekayasa Perangkat Lunak
Real-time Inventory Dark Store: Cegah Oversell di Q-Commerce

Real-time Inventory Dark Store: Cegah Oversell di Q-Commerce

Desain inventory per-hub untuk Astro (instant grocery). Reservation pattern dengan Redis DECRBY atomic + TTL, async sync ke Postgres, stock partitioning untuk hot SKU, reaper job untuk reservation leak, dan cycle counting untuk stok fisik vs sistem. Implementasi Golang lengkap.
Faisal Affan
Faisal Affan
Diterbitkan

Promo & Voucher Engine Q-Commerce: Rule Engine Anti-Abuse

System design promo engine untuk quick commerce. Rule engine dengan kondisi komposable (min transaksi, first-N-order, area, kategori) ke efek (discount, free ongkir), atomic counter untuk limit penggunaan Redis Lua, anti-fraud device fingerprinting, stackable vs eksklusif promo resolution, dan budget cap. Implementasi Golang lengkap dengan composable condition interface, AND/OR composite tree, concurrent-safe redeem, serta Kafka event streaming untuk audit trail.
Faisal Affan
Faisal Affan
Diterbitkan