12. Integer to Roman
Problem Description
You need to convert an integer into its Roman numeral representation.
Roman numerals use seven different symbols with specific values:
I
= 1V
= 5X
= 10L
= 50C
= 100D
= 500M
= 1000
The conversion follows these rules:
-
Basic Rule: Build the Roman numeral by converting decimal place values from highest to lowest. For most values, repeatedly use the largest possible symbol that doesn't exceed the remaining number.
-
Subtractive Form: When a decimal place value starts with 4 or 9, use a special subtractive notation:
- 4 →
IV
(1 before 5) - 9 →
IX
(1 before 10) - 40 →
XL
(10 before 50) - 90 →
XC
(10 before 100) - 400 →
CD
(100 before 500) - 900 →
CM
(100 before 1000)
- 4 →
-
Repetition Limits: Powers of 10 (
I
,X
,C
,M
) can appear at most 3 times consecutively. The symbolsV
,L
, andD
cannot be repeated. If you would need 4 of the same symbol, use the subtractive form instead.
For example:
- 3 →
III
(three 1's) - 4 →
IV
(notIIII
) - 58 →
LVIII
(50 + 5 + 3) - 1994 →
MCMXCIV
(1000 + 900 + 90 + 4)
Given an integer num
, return its Roman numeral representation as a string.
Intuition
When converting a number to Roman numerals, we naturally think about how we would do it manually. We'd start with the largest symbols and work our way down, using as many of each symbol as possible before moving to smaller ones.
The key insight is that we need to handle both regular symbols and the special subtractive cases (like 4, 9, 40, 90, 400, 900). Instead of writing complex conditional logic to detect when we need subtractive notation, we can treat these special cases as standalone "symbols" with their own values.
For instance, rather than checking if we have a 4 or 9 pattern, we can simply include IV
(4), IX
(9), XL
(40), XC
(90), CD
(400), and CM
(900) in our list of available symbols alongside the regular ones.
This transforms the problem into a simple greedy algorithm: we create a list of all symbols (including the two-character subtractive forms) ordered by value from largest to smallest. Then we iterate through this list, using each symbol as many times as possible before moving to the next smaller one.
For example, with the number 1994:
- Use
M
once (1000), leaving 994 - Use
CM
once (900), leaving 94 - Use
XC
once (90), leaving 4 - Use
IV
once (4), leaving 0 - Result:
MCMXCIV
This greedy approach works because Roman numeral notation is designed to represent numbers using the fewest symbols possible, always preferring larger values when available. By including the subtractive forms in our symbol list, we ensure they're automatically used when appropriate without any special case handling.
Learn more about Math patterns.
Solution Approach
The implementation uses a greedy algorithm with pre-defined symbol-value pairs. Here's how it works:
Step 1: Define Symbol-Value Mappings
We create two tuples that store all possible Roman numeral representations in descending order by value:
cs
contains the symbols:('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
vs
contains the values:(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
Notice how both regular symbols and subtractive forms are included in the same list.
Step 2: Greedy Conversion Process
We initialize an empty list ans
to build our result. Then we iterate through each symbol-value pair using zip(cs, vs)
:
For each pair (c, v)
:
- While
num >= v
, we can use this symbol - Subtract the value
v
fromnum
- Append the symbol
c
to our result list - Repeat until
num
is less than the current value
Step 3: Build Final String
After processing all symbols, we join the list of symbols into a single string using ''.join(ans)
.
Example Walkthrough with num = 58:
- Start with
M
(1000): 58 < 1000, skip - Continue through larger values until
L
(50): 58 >= 50- Add 'L', num becomes 8
- Skip values until
V
(5): 8 >= 5- Add 'V', num becomes 3
- Skip to
I
(1): 3 >= 1- Add 'I' three times, num becomes 0
- Result:
'LVIII'
The algorithm's time complexity is O(1)
since we have a fixed number of symbols (13), and the space complexity is also O(1)
as the maximum length of any Roman numeral within the typical integer range is bounded.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through converting the number 94 to Roman numerals using our greedy approach.
Setup: We have our symbol-value pairs ordered 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)
Step-by-step process:
-
Start with num = 94, result = ""
- Check
M
(1000): 94 < 1000 → skip - Check
CM
(900): 94 < 900 → skip - Check
D
(500): 94 < 500 → skip - Check
CD
(400): 94 < 400 → skip - Check
C
(100): 94 < 100 → skip
- Check
-
Reach
XC
(90): 94 ≥ 90 ✓- Add "XC" to result
- Subtract 90 from num: 94 - 90 = 4
- Result = "XC", num = 4
-
Continue with num = 4
- Check
L
(50): 4 < 50 → skip - Check
XL
(40): 4 < 40 → skip - Check
X
(10): 4 < 10 → skip - Check
IX
(9): 4 < 9 → skip - Check
V
(5): 4 < 5 → skip
- Check
-
Reach
IV
(4): 4 ≥ 4 ✓- Add "IV" to result
- Subtract 4 from num: 4 - 4 = 0
- Result = "XCIV", num = 0
-
num = 0, conversion complete
Final answer: "XCIV"
Notice how the algorithm automatically uses the subtractive form XC
for 90 and IV
for 4, without any special conditional logic. By including these two-character combinations in our symbol list, the greedy approach naturally selects them when appropriate, resulting in the correct and most concise Roman numeral representation.
Solution Implementation
1class Solution:
2 def intToRoman(self, num: int) -> str:
3 """
4 Convert an integer to Roman numeral representation.
5
6 Args:
7 num: Integer to convert (1 <= num <= 3999)
8
9 Returns:
10 String representation of the Roman numeral
11 """
12 # Define Roman numeral symbols and their corresponding values
13 # Ordered from largest to smallest, including subtractive notation cases
14 roman_symbols = ('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
15 decimal_values = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
16
17 # List to collect Roman numeral parts
18 result = []
19
20 # Iterate through each Roman symbol and its value
21 for symbol, value in zip(roman_symbols, decimal_values):
22 # Greedily subtract the largest possible value
23 while num >= value:
24 num -= value
25 result.append(symbol)
26
27 # Join all parts into final Roman numeral string
28 return ''.join(result)
29
1class Solution {
2 /**
3 * Converts an integer to its Roman numeral representation.
4 *
5 * @param num The integer to convert (1 <= num <= 3999)
6 * @return The Roman numeral representation as a string
7 */
8 public String intToRoman(int num) {
9 // Define Roman numeral symbols in descending order of value
10 // Include subtractive notation pairs (CM, CD, XC, XL, IX, IV)
11 List<String> romanSymbols = List.of(
12 "M", // 1000
13 "CM", // 900
14 "D", // 500
15 "CD", // 400
16 "C", // 100
17 "XC", // 90
18 "L", // 50
19 "XL", // 40
20 "X", // 10
21 "IX", // 9
22 "V", // 5
23 "IV", // 4
24 "I" // 1
25 );
26
27 // Corresponding integer values for each Roman symbol
28 List<Integer> values = List.of(
29 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1
30 );
31
32 // StringBuilder for efficient string concatenation
33 StringBuilder result = new StringBuilder();
34
35 // Iterate through each Roman symbol-value pair
36 for (int i = 0; i < romanSymbols.size(); i++) {
37 // Repeatedly subtract the current value while possible
38 // and append the corresponding Roman symbol
39 while (num >= values.get(i)) {
40 num -= values.get(i);
41 result.append(romanSymbols.get(i));
42 }
43 }
44
45 return result.toString();
46 }
47}
48
1class Solution {
2public:
3 string intToRoman(int num) {
4 // Define Roman numeral symbols and their corresponding values
5 // Ordered from largest to smallest, including subtractive notation cases
6 vector<string> romanSymbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
7 vector<int> values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
8
9 string result;
10
11 // Iterate through each Roman numeral symbol
12 for (int i = 0; i < romanSymbols.size(); ++i) {
13 // While the current value can be subtracted from num
14 while (num >= values[i]) {
15 // Subtract the value and append the corresponding Roman symbol
16 num -= values[i];
17 result += romanSymbols[i];
18 }
19 }
20
21 return result;
22 }
23};
24
1/**
2 * Converts an integer to its Roman numeral representation
3 * @param num - The integer to convert (1 <= num <= 3999)
4 * @returns The Roman numeral string representation
5 */
6function intToRoman(num: number): string {
7 // Roman numeral symbols in descending order of value
8 // Including subtractive notation pairs (CM, CD, XC, XL, IX, IV)
9 const romanSymbols: string[] = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
10
11 // Corresponding integer values for each Roman symbol
12 const integerValues: number[] = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
13
14 // Array to accumulate the Roman numeral characters
15 const result: string[] = [];
16
17 // Iterate through each Roman symbol-value pair
18 for (let i = 0; i < integerValues.length; i++) {
19 // Add the current Roman symbol as many times as possible
20 while (num >= integerValues[i]) {
21 num -= integerValues[i];
22 result.push(romanSymbols[i]);
23 }
24 }
25
26 // Join all Roman symbols into a single string
27 return result.join('');
28}
29
Time and Space Complexity
The time complexity is O(m)
, where m
is the number of symbols (13 in this case). Although there's a nested structure with the for
loop and while
loop, the total number of iterations across all symbols is bounded. Each unit of the input number can only be "consumed" once - we subtract from num
until it reaches 0. Since the maximum input is 3999 (per Roman numeral constraints), and each subtraction reduces num
, the total work done is proportional to the number of symbols times a constant factor. Therefore, the time complexity is O(m)
.
The space complexity is O(m)
as well. We store two tuples (cs
and vs
) each containing m
symbols and their corresponding values. The ans
list in the worst case could contain multiple copies of symbols, but the total length is still bounded by a constant multiple of m
(for example, the number 3888 would produce "MMMDCCCLXXXVIII", which has a length proportional to the number of symbol categories used). The final string created by ''.join(ans)
also has space proportional to m
. Thus, the space complexity is O(m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Handle Subtractive Cases Separately
A common mistake is trying to detect and handle subtractive cases (4, 9, 40, 90, 400, 900) with special conditional logic instead of including them in the mapping.
Problematic Approach:
def intToRoman(self, num: int) -> str:
result = []
# Wrong: Trying to detect patterns manually
while num > 0:
if num >= 1000:
result.append('M')
num -= 1000
elif num >= 900: # Special case detection
result.append('CM')
num -= 900
elif num >= 500:
result.append('D')
num -= 500
elif num >= 400: # Another special case
result.append('CD')
num -= 400
# ... many more if-elif statements
This approach leads to:
- Verbose, error-prone code with many conditional branches
- Difficulty maintaining and debugging the logic
- Higher chance of missing edge cases
Solution: Include all subtractive forms directly in the symbol-value mappings and process them uniformly with the greedy algorithm.
2. Using Modulo and Division Instead of Subtraction
Some might try to optimize by calculating how many times each symbol appears using division and modulo operations.
Problematic Approach:
def intToRoman(self, num: int) -> str:
symbols = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
result = []
for i, value in enumerate(values):
count = num // value # How many times this symbol appears
if count:
result.append(symbols[i] * count)
num = num % value # Wrong: loses track of remaining value
While this might seem cleaner, it:
- Makes the code less intuitive
- Can introduce subtle bugs if not handled carefully
- Doesn't provide significant performance benefits for this problem
Solution: Stick with the simple subtraction approach which is clearer and equally efficient.
3. Incorrect Symbol Ordering
Placing symbols in the wrong order (not strictly descending by value) breaks the greedy algorithm.
Problematic Approach:
# Wrong order - smaller values before larger ones symbols = ('I', 'IV', 'V', 'IX', 'X', 'XL', 'L', 'XC', 'C', 'CD', 'D', 'CM', 'M') values = (1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000)
This would cause the algorithm to use smaller denominations first, producing incorrect results like using many I's instead of larger symbols.
Solution: Always maintain strict descending order by value in your mappings.
4. String Concatenation Instead of List Building
Using string concatenation in the loop is inefficient in Python.
Problematic Approach:
result = "" # String instead of list
for symbol, value in zip(roman_symbols, decimal_values):
while num >= value:
num -= value
result += symbol # String concatenation - inefficient
String concatenation creates a new string object each time, leading to O(n²) time complexity for building the result.
Solution: Use a list to collect symbols and join them at the end with ''.join()
for O(n) complexity.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!