LeetCode 12 - Integer to Roman
The problem asks us to convert a positive integer into its Roman numeral representation. Roman numerals use combinations of specific symbols to represent values, and the rules for combining those symbols are strict.
Difficulty: 🟡 Medium
Topics: Hash Table, Math, String
Solution
Problem Understanding
The problem asks us to convert a positive integer into its Roman numeral representation. Roman numerals use combinations of specific symbols to represent values, and the rules for combining those symbols are strict. Unlike a positional number system such as decimal, Roman numerals are constructed greedily from the largest possible symbol values down to the smallest.
The input is a single integer num, where 1 <= num <= 3999. The output should be a string representing the equivalent Roman numeral.
The Roman numeral system uses these symbols:
| Symbol | Value |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
Most Roman numerals are formed by repeatedly subtracting the largest possible value from the remaining number and appending the corresponding symbol. However, there are special subtractive cases. Instead of writing four identical symbols in a row, Roman numerals use compact subtractive forms:
| Value | Roman |
|---|---|
| 4 | IV |
| 9 | IX |
| 40 | XL |
| 90 | XC |
| 400 | CD |
| 900 | CM |
For example, 4 is written as IV instead of IIII, and 900 is written as CM instead of DCCCC.
The constraint limit of 3999 is important because standard Roman numerals traditionally stop there. This means the solution only needs to support symbols up to M.
Several edge cases can cause mistakes in a naive implementation. Numbers containing 4 or 9 in any decimal place require subtractive notation. For example, 49 must become XLIX, not IL. Another important case is repeated symbols. Roman numerals allow at most three consecutive repetitions of I, X, C, and M. Symbols like V, L, and D cannot repeat. The problem guarantees that the input is always valid and within range, so we do not need to handle invalid or negative numbers.
Approaches
Brute Force Approach
A brute force solution could attempt to simulate Roman numeral construction digit by digit using many conditional checks for thousands, hundreds, tens, and ones places separately. For each decimal place, the algorithm would repeatedly append symbols according to Roman numeral rules.
For example, if the number is 3749, the algorithm would process:
- Thousands digit
3→"MMM" - Hundreds digit
7→"DCC" - Tens digit
4→"XL" - Ones digit
9→"IX"
This approach works because Roman numerals are fundamentally based on decimal place decomposition. However, implementing every rule manually with nested conditions becomes verbose and error-prone. It also duplicates logic across place values.
Although the runtime is still small due to the constraint limit, the implementation complexity is unnecessarily high.
Optimal Greedy Approach
The key insight is that Roman numerals can be built greedily from largest value to smallest value.
At every step, we choose the largest Roman numeral value that does not exceed the remaining number. We append its symbol to the result and subtract its value from the number. This naturally handles both standard and subtractive forms.
For example:
1994- Largest value ≤ 1994 is
1000→ append"M" - Remaining =
994 - Largest value ≤ 994 is
900→ append"CM" - Remaining =
94 - Largest value ≤ 94 is
90→ append"XC" - Remaining =
4 - Largest value ≤ 4 is
4→ append"IV"
Result = "MCMXCIV"
This works cleanly because Roman numerals are designed around descending value composition.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(1) | O(1) | Uses many manual conditional rules for each digit place |
| Optimal | O(1) | O(1) | Greedy construction using predefined value-symbol pairs |
Even though both are technically constant time because the input range is bounded, the greedy solution is cleaner, more scalable, and easier to reason about.
Algorithm Walkthrough
- Create an ordered list of Roman numeral value-symbol pairs.
The list must include both standard symbols and subtractive forms. The pairs are arranged from largest to smallest value so the algorithm can greedily consume the number.
Example:
1000 -> M
900 -> CM
500 -> D
400 -> CD
...
1 -> I
- Initialize an empty result string.
This string will store the Roman numeral as we construct it. 3. Iterate through the value-symbol pairs in descending order.
For each pair, check whether the current number is greater than or equal to the value. 4. While the current value fits into the remaining number:
- Append the corresponding Roman symbol to the result.
- Subtract the value from the number.
This greedy process ensures we always use the largest possible Roman numeral representation first. 5. Continue until the remaining number becomes zero.
Once the number reaches zero, the Roman numeral is complete. 6. Return the constructed result string.
Why it works
The algorithm works because Roman numerals are fundamentally a greedy representation system. Every valid Roman numeral is constructed by repeatedly taking the largest possible legal symbol or subtractive pair. Since the list includes all required subtractive forms and is ordered from largest to smallest, the algorithm always produces the canonical Roman numeral representation without violating Roman numeral rules.
Python Solution
class Solution:
def intToRoman(self, num: int) -> str:
value_symbols = [
(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"),
]
result = []
for value, symbol in value_symbols:
while num >= value:
result.append(symbol)
num -= value
return "".join(result)
The implementation starts by defining the Roman numeral mappings in descending order. Including subtractive forms such as CM and IV is essential because these special cases cannot be derived automatically from the standard symbols.
The result list is used instead of repeated string concatenation because appending to a list is efficient in Python. At the end, "".join(result) constructs the final Roman numeral string.
The main loop iterates through each value-symbol pair. For every pair, the inner while loop repeatedly applies the symbol as long as the remaining number is large enough.
For example, if num = 3000, the algorithm appends "M" three times before moving to the next value.
This implementation directly follows the greedy algorithm described earlier.
Go Solution
func intToRoman(num int) string {
valueSymbols := []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"},
}
result := ""
for _, pair := range valueSymbols {
for num >= pair.value {
result += pair.symbol
num -= pair.value
}
}
return result
}
The Go implementation mirrors the Python logic closely. Since Go does not have tuples, the mapping is stored as a slice of anonymous structs containing value and symbol.
String concatenation is acceptable here because the Roman numeral length is very small, bounded by the problem constraints. In larger string-building problems, a strings.Builder would usually be preferred for efficiency.
Go integers easily handle values up to 3999, so overflow is not a concern.
Worked Examples
Example 1: num = 3749
| Step | Current Value | Symbol | Remaining Number | Result |
|---|---|---|---|---|
| 1 | 1000 | M | 2749 | M |
| 2 | 1000 | M | 1749 | MM |
| 3 | 1000 | M | 749 | MMM |
| 4 | 500 | D | 249 | MMMD |
| 5 | 100 | C | 149 | MMMDC |
| 6 | 100 | C | 49 | MMMDCC |
| 7 | 40 | XL | 9 | MMMDCCXL |
| 8 | 9 | IX | 0 | MMMDCCXLIX |
Final result:
MMMDCCXLIX
Example 2: num = 58
| Step | Current Value | Symbol | Remaining Number | Result |
|---|---|---|---|---|
| 1 | 50 | L | 8 | L |
| 2 | 5 | V | 3 | LV |
| 3 | 1 | I | 2 | LVI |
| 4 | 1 | I | 1 | LVII |
| 5 | 1 | I | 0 | LVIII |
Final result:
LVIII
Example 3: num = 1994
| Step | Current Value | Symbol | Remaining Number | Result |
|---|---|---|---|---|
| 1 | 1000 | M | 994 | M |
| 2 | 900 | CM | 94 | MCM |
| 3 | 90 | XC | 4 | MCMXC |
| 4 | 4 | IV | 0 | MCMXCIV |
Final result:
MCMXCIV
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) | The number of Roman numeral pairs is fixed and bounded |
| Space | O(1) | Output size is bounded by Roman numeral length |
Although the algorithm contains nested loops, the total amount of work is limited because Roman numerals for numbers up to 3999 are very short. The mapping list always contains exactly 13 entries, so the runtime does not grow with input size in practice.
The output string itself also has a bounded maximum length, making space usage constant as well.
Test Cases
solution = Solution()
assert solution.intToRoman(1) == "I" # smallest valid input
assert solution.intToRoman(3) == "III" # repeated symbols
assert solution.intToRoman(4) == "IV" # subtractive notation
assert solution.intToRoman(9) == "IX" # subtractive notation
assert solution.intToRoman(14) == "XIV" # mixed standard and subtractive
assert solution.intToRoman(40) == "XL" # subtractive tens
assert solution.intToRoman(58) == "LVIII" # example case
assert solution.intToRoman(90) == "XC" # subtractive tens
assert solution.intToRoman(400) == "CD" # subtractive hundreds
assert solution.intToRoman(900) == "CM" # subtractive hundreds
assert solution.intToRoman(944) == "CMXLIV" # multiple subtractive forms
assert solution.intToRoman(1994) == "MCMXCIV" # example case
assert solution.intToRoman(3749) == "MMMDCCXLIX" # example case
assert solution.intToRoman(3999) == "MMMCMXCIX" # largest valid input
| Test | Why |
|---|---|
1 |
Validates smallest boundary |
3 |
Verifies repeated symbols |
4 |
Tests subtractive notation |
9 |
Tests subtractive notation |
14 |
Tests mixed representation |
40 |
Tests subtractive tens |
58 |
Confirms provided example |
90 |
Tests subtractive tens |
400 |
Tests subtractive hundreds |
900 |
Tests subtractive hundreds |
944 |
Validates multiple subtractive forms together |
1994 |
Confirms provided example |
3749 |
Confirms provided example |
3999 |
Validates upper boundary |
Edge Cases
One important edge case is numbers containing 4 or 9 in any decimal place. Roman numerals use subtractive notation for these values, so 4 becomes IV instead of IIII, and 90 becomes XC instead of LXXXX. A naive implementation that repeatedly appends symbols without accounting for subtractive forms would generate invalid Roman numerals. This implementation handles the issue correctly by explicitly including subtractive pairs in the mapping list.
Another critical edge case is the maximum valid input, 3999. This number produces the longest standard Roman numeral: "MMMCMXCIX". Some incorrect implementations fail because they do not process symbols in descending order or forget to include 900 and 90 mappings. Since this algorithm greedily consumes values from largest to smallest, it correctly constructs the numeral without ambiguity.
A third edge case involves repeated symbols. Roman numeral rules allow at most three consecutive repetitions of I, X, C, and M. Values such as 4, 40, and 400 must therefore use subtractive notation instead of four repeated symbols. By including IV, XL, and CD directly in the lookup table, the implementation automatically avoids invalid repetitions.
A final subtle case is numbers like 49. Some incorrect solutions attempt to use non-standard subtractive forms such as IL. Roman numerals must be constructed independently by decimal place, so 49 must become XLIX. Because the greedy mapping includes only legal Roman numeral combinations, the algorithm always generates valid canonical forms.