LeetCode #12 — Medium

Integer to Roman

A complete visual walkthrough of the greedy decomposition approach — from brute force to an elegant table-driven solution.

Solve on LeetCode
The Problem

Problem Statement

Seven different symbols represent Roman numerals with the following values:

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

Roman numerals are formed by appending the conversions of decimal place values from highest to lowest. Converting a decimal place value into a Roman numeral has the following rules:

  • If the value does not start with 4 or 9, select the symbol of the maximal value that can be subtracted from the input, append that symbol to the result, subtract its value, and convert the remainder to a Roman numeral.
  • If the value starts with 4 or 9 use the subtractive form representing one symbol subtracted from the following symbol, for example, 4 is 1 (I) less than 5 (V): IV and 9 is 1 (I) less than 10 (X): IX. Only the following subtractive forms are used: 4 (IV), 9 (IX), 40 (XL), 90 (XC), 400 (CD) and 900 (CM).
  • Only powers of 10 (I, X, C, M) can be appended consecutively at most 3 times to represent multiples of 10. You cannot append 5 (V), 50 (L), or 500 (D) multiple times. If you need to append a symbol 4 times use the subtractive form.

Given an integer, convert it to a Roman numeral.

Example 1:

Input: num = 3749

Output: "MMMDCCXLIX"

Explanation:

3000 = MMM as 1000 (M) + 1000 (M) + 1000 (M)
 700 = DCC as 500 (D) + 100 (C) + 100 (C)
  40 = XL as 10 (X) less of 50 (L)
   9 = IX as 1 (I) less of 10 (X)
Note: 49 is not 1 (I) less of 50 (L) because the conversion is based on decimal places

Example 2:

Input: num = 58

Output: "LVIII"

Explanation:

50 = L
 8 = VIII

Example 3:

Input: num = 1994

Output: "MCMXCIV"

Explanation:

1000 = M
 900 = CM
  90 = XC
   4 = IV

Constraints:

  • 1 <= num <= 3999

Roadmap

  1. The Brute Force Approach
  2. The Core Insight — Greedy Decomposition
  3. Algorithm Walkthrough
  4. Edge Cases
  5. Full Annotated Code
  6. Interactive Demo
  7. Complexity Analysis
Step 01

The Brute Force Approach

The naive approach processes each digit position independently with cascading if/else chains. Extract the thousands, hundreds, tens, and ones, then handle each one separately.

Digit-by-Digit with Conditionals

class Solution {
    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();

        // Thousands (only M, up to 3)
        int thousands = num / 1000;
        for (int i = 0; i < thousands; i++) sb.append("M");
        num %= 1000;

        // Hundreds (need to handle 400, 500, 900...)
        int hundreds = num / 100;
        if (hundreds == 9)      sb.append("CM");
        else if (hundreds >= 5) { sb.append("D");
            for (int i = 5; i < hundreds; i++) sb.append("C");
        } else if (hundreds == 4) sb.append("CD");
        else for (int i = 0; i < hundreds; i++) sb.append("C");
        num %= 100;

        // Tens — same pattern all over again...
        int tens = num / 10;
        if (tens == 9)      sb.append("XC");
        else if (tens >= 5) { sb.append("L");
            for (int i = 5; i < tens; i++) sb.append("X");
        } else if (tens == 4) sb.append("XL");
        else for (int i = 0; i < tens; i++) sb.append("X");
        num %= 10;

        // Ones — yet again...
        // ... you get the picture
    }
}

The same if/else pattern repeats four times for thousands, hundreds, tens, and ones. It works, but it is verbose, error-prone, and hard to maintain. There must be a better way.

💡

The repeated structure is a signal. Whenever you see the same logic repeated with different constants, that logic can usually be driven by a data table instead of code.

Step 02

The Core Insight — Greedy Decomposition

Roman numerals are inherently greedy: always use the largest possible symbol first, then move to smaller ones. The key insight is to include the six subtractive forms (IV, IX, XL, XC, CD, CM) as first-class entries in your lookup table.

The Complete Value Table (13 Entries)

All 7 standard symbols + 6 subtractive forms, sorted from largest to smallest:

M 1000
CM 900
D 500
CD 400
C 100
XC 90
L 50
XL 40
X 10
IX 9
V 5
IV 4
I 1

Why does greedy work here? Roman numerals are designed so that each symbol's value is never more than 10x the next one, and the subtractive forms fill in the gaps. This guarantees that always choosing the largest possible symbol produces the correct (and unique) representation. No backtracking needed.

Greedy in Action: Decomposing 1994

Start with 1994. Scan the table from left to right. If the current value fits, subtract it and append its symbol. Repeat until the number reaches zero.

M
1000
+
CM
900
+
XC
90
+
IV
4
=
1994
total
Step 03

Algorithm Walkthrough

Let's trace the greedy algorithm step by step on num = 1994. At each step we check: is num >= current value? If yes, subtract and append. If no, move to the next entry.

Tracing num = 1994

1 1994 >= 1000 (M)? Yes. Subtract 1000, append M rem = 994
2 994 >= 1000 (M)? No. Move on. rem = 994
3 994 >= 900 (CM)? Yes. Subtract 900, append CM rem = 94
4 94 >= 500 (D)? No. 94 >= 400 (CD)? No. 94 >= 100 (C)? No. Move on. rem = 94
5 94 >= 90 (XC)? Yes. Subtract 90, append XC rem = 4
6 4 >= 50 (L)? No. 4 >= 40 (XL)? No. 4 >= 10 (X)? No. 4 >= 9 (IX)? No. 4 >= 5 (V)? No. Move on. rem = 4
7 4 >= 4 (IV)? Yes. Subtract 4, append IV rem = 0
Result
M
CM
XC
IV
= "MCMXCIV"
🎯

Notice the simplicity. One loop, one table, no conditionals about digit positions. The subtractive forms (CM, XC, IV) are handled automatically because they appear in the table before the smaller standard symbols. The algorithm does not even know they are "special."

Step 04

Edge Cases

The greedy algorithm handles all cases uniformly, but it is worth verifying the boundaries.

Key Test Cases

num = 1
"I"
Minimum valid input. Single symbol.
num = 3999
"MMMCMXCIX"
Maximum valid input. Uses all place values.
num = 3
"III"
Repeated symbol, no subtractive needed.
num = 58
"LVIII"
L + V + III. Mix of symbols.
num = 1994
"MCMXCIV"
Multiple subtractive forms (CM, XC, IV).
num = 444
"CDXLIV"
All subtractive forms at hundreds, tens, ones.
💡

Why does 3999 work? The table starts with M=1000, and the while loop appends M three times for 3000. Then CM handles 900, XC handles 90, and IX handles 9. The algorithm naturally composes any number in the valid range [1, 3999] without special-casing.

Step 05

Full Annotated Code

class Solution {
    public String intToRoman(int num) {

        // All 13 values: 7 standard + 6 subtractive, descending
        int[] values =    {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
        String[] symbols = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};

        StringBuilder sb = new StringBuilder();

        // Greedy: try each value from largest to smallest
        for (int i = 0; i < values.length; i++) {

            // While the current value still fits, keep using it
            while (num >= values[i]) {
                sb.append(symbols[i]);   // Append the Roman symbol
                num -= values[i];         // Subtract its value
            }
            // When it no longer fits, move to the next smaller value
        }

        return sb.toString();       // e.g. "MCMXCIV" for 1994
    }
}
📝

Why StringBuilder? String concatenation in a loop creates a new String object each time (O(n) per append). StringBuilder mutates in place, keeping overall construction at O(n) where n is the length of the result (at most 15 characters for 3888 = "MMMDCCCLXXXVIII").

Step 06

Interactive Demo

Enter any number from 1 to 3999 and step through the greedy decomposition. Watch which symbol is chosen at each step and how the remaining value decreases.

⚙ Greedy Decomposition Visualizer


Step 07

Complexity Analysis

Time
O(m)
Space
O(m)

The outer loop iterates over exactly 13 table entries. The inner while loop runs a bounded number of times per entry (at most 3 for M, C, X, I -- never more). Since the input is constrained to [1, 3999], the total number of operations is bounded by a constant (~39 iterations worst case). The output string is at most 15 characters long.

Approach Breakdown

IF/ELSE PER DIGIT
O(1) time
O(1) space

Four separate if/else blocks handle thousands, hundreds, tens, and ones. Each block runs a bounded number of comparisons and appends. Since the input is capped at 3999, the total work is constant, but the code is verbose with 40+ lines of repetitive branching.

HARDCODED TABLE
O(1) time
O(1) space

Precomputes four lookup arrays (ones[0..9], tens[0..9], hundreds[0..9], thousands[0..3]) and indexes directly by digit. Each digit position is a single array lookup, giving constant time. The four arrays use fixed storage of roughly 40 entries total.

GREEDY TABLE
O(1) time
O(1) space

One outer loop over 13 table entries and an inner while loop that subtracts each value repeatedly. The inner loop runs at most 3 times per entry (for M, C, X, I), capping total iterations at roughly 39. The 13-entry table is fixed-size and the output string is at most 15 characters.

🎯

The input is bounded (1-3999), so runtime is constant regardless. All approaches achieve O(1), but the greedy table approach replaces 40+ lines of repetitive if/else logic with a single loop over 13 entries. The "win" here is not speed but clarity and maintainability -- data-driven code over hard-coded branches.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Overflow in intermediate arithmetic

Wrong move: Temporary multiplications exceed integer bounds.

Usually fails on: Large inputs wrap around unexpectedly.

Fix: Use wider types, modular arithmetic, or rearranged operations.