LeetCode #13 — Easy

Roman to Integer

A complete visual walkthrough of converting Roman numerals to integers — from brute force to the elegant one-pass solution.

Solve on LeetCode
The Problem

Problem Statement

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

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

For example, 2 is written as II in Roman numeral, just two ones added together. 12 is written as XII, which is simply X + II. The number 27 is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:

  • I can be placed before V (5) and X (10) to make 4 and 9. 
  • X can be placed before L (50) and C (100) to make 40 and 90. 
  • C can be placed before D (500) and M (1000) to make 400 and 900.

Given a roman numeral, convert it to an integer.

Example 1:

Input: s = "III"
Output: 3
Explanation: III = 3.

Example 2:

Input: s = "LVIII"
Output: 58
Explanation: L = 50, V= 5, III = 3.

Example 3:

Input: s = "MCMXCIV"
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.

Constraints:

  • 1 <= s.length <= 15
  • s contains only the characters ('I', 'V', 'X', 'L', 'C', 'D', 'M').
  • It is guaranteed that s is a valid roman numeral in the range [1, 3999].

Roadmap

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

The Brute Force Approach

Roman numerals have seven symbols. Most of the time you just add them up. The tricky part is the six subtractive combinations like IV (4), IX (9), XL (40), XC (90), CD (400), and CM (900).

The Symbol Map

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

A brute force approach would explicitly check for every two-character subtractive pair before handling single characters:

// Brute force: check every pair manually
if (s.startsWith("CM", i)) { result += 900; i += 2; }
else if (s.startsWith("CD", i)) { result += 400; i += 2; }
else if (s.startsWith("XC", i)) { result += 90; i += 2; }
else if (s.startsWith("XL", i)) { result += 40; i += 2; }
else if (s.startsWith("IX", i)) { result += 9; i += 2; }
else if (s.startsWith("IV", i)) { result += 4; i += 2; }
else { result += map(s.charAt(i)); i += 1; }

This works, but it is verbose, error-prone, and hard to extend. Six special cases, each handled explicitly. There is a much more elegant pattern hiding in plain sight.

💡

Notice the pattern: In every subtractive case (IV, IX, XL, XC, CD, CM), a smaller value appears immediately before a larger value. That observation is the key to everything.

Step 02

The Core Insight — One Simple Rule

Instead of checking for specific pairs, we can use a single universal rule that handles all cases automatically:

The Rule

Scan left to right. If the current value is less than the next value,
subtract it. Otherwise, add it.

Let's see why this works with "MCMXCIV" (1994):

Visual: Add or Subtract?

M
1000
+ADD
M ≥ C
C
100
-SUB
C < M
M
1000
+ADD
M ≥ X
X
10
-SUB
X < C
C
100
+ADD
C ≥ I
I
1
-SUB
I < V
V
5
+ADD
last char
+1000 -100 +1000 -10 +100 -1 +5 = 1994

Why does this work? Roman numerals are written largest-to-smallest (left to right). When a smaller value precedes a larger one, it always means subtraction (like IV = 4, not 1+5). The rule exploits this structural property to eliminate all special-case logic.

Step 03

Algorithm Walkthrough

Let's trace through "MCMXCIV" step by step, watching the running total build up to 1994:

Step-by-Step Trace

i Char Value Next curr < next? Action Running Total
0 M 1000 C (100) 1000 < 100? No + 1000 1000
1 C 100 M (1000) 100 < 1000? Yes - 100 900
2 M 1000 X (10) 1000 < 10? No + 1000 1900
3 X 10 C (100) 10 < 100? Yes - 10 1890
4 C 100 I (1) 100 < 1? No + 100 1990
5 I 1 V (5) 1 < 5? Yes - 1 1989
6 V 5 none (0) 5 < 0? No + 5 1994
🎯

Notice the pairs: The subtracted C(-100) and the following M(+1000) together give CM = 900. Likewise, X(-10) + C(+100) = XC = 90, and I(-1) + V(+5) = IV = 4. The rule naturally decomposes 1994 = 1000 + 900 + 90 + 4.

Step 04

Edge Cases

The beauty of the one-rule approach is that it handles every case uniformly. Still, it is worth verifying the boundaries:

Important Cases to Consider

"I" 1 Single character, no next value to compare
"III" 3 All same characters, always add (1+1+1)
"LVIII" 58 No subtractive cases (50+5+1+1+1)
"IV" 4 Minimal subtractive case (-1+5)
"MMMCMXCIX" 3999 Maximum valid Roman numeral
📝

Constraint note: The problem guarantees the input is a valid Roman numeral in the range [1, 3999]. No need to validate the input string, but in a production system you would add checks for invalid characters and malformed sequences.

Step 05

Full Annotated Code

class Solution {
    public int romanToInt(String s) {

        // Map each Roman symbol to its integer value
        Map<Character, Integer> map = Map.of(
            'I', 1,    'V', 5,
            'X', 10,   'L', 50,
            'C', 100,  'D', 500,
            'M', 1000
        );

        int result = 0;

        for (int i = 0; i < s.length(); i++) {

            // Get the value of the current character
            int curr = map.get(s.charAt(i));

            // Peek at the next character (0 if at end)
            int next = (i + 1 < s.length())
                     ? map.get(s.charAt(i + 1))
                     : 0;

            // THE RULE: smaller before larger → subtract
            if (curr < next)
                result -= curr;   // subtractive case (IV, IX, XL...)
            else
                result += curr;   // normal additive case
        }

        return result;
    }
}
💡

Why left-to-right? This approach scans forward, comparing each character with the next. Alternatively, you can scan right-to-left and compare with the previous value. Both achieve O(n) with O(1) space. The left-to-right version shown here is arguably the most readable.

Step 06

Interactive Demo

Enter any Roman numeral and step through the algorithm character by character. Watch the add/subtract decisions and running total update in real time.

⚙ Roman Numeral Converter


Step 07

Complexity Analysis

Time
O(n)
Space
O(m)

We make a single pass through the string of length n, doing O(1) work per character: one hash map lookup for the current value, one for the next value, and one comparison to decide add or subtract. The hash map itself has exactly 7 entries, so it uses constant space regardless of input size.

Approach Breakdown

PAIR CHECKING
O(n) time
O(1) space

A single loop scans left to right with six explicit if/else branches checking for two-character subtractive pairs (CM, CD, XC, XL, IX, IV). Each match consumes two characters, otherwise one. The loop touches each character once for O(n), using only a result integer and index variable.

LOOKAHEAD RULE
O(n) time
O(1) space

One pass through the string with a single comparison per character: look up the current and next values in a 7-entry hash map, then add or subtract based on whether current is less than next. Two O(1) map lookups per iteration yield O(n) total, with only the fixed-size map and a result variable in memory.

🎯

The subtraction rule (IV, IX, XL, XC, CD, CM) adds exactly one lookahead comparison per character. Instead of six explicit if/else branches for subtractive pairs, the simple rule "if current < next, subtract" handles all cases with zero special-case code. Both approaches are O(n), but the lookahead version is cleaner and impossible to get wrong.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Mutating counts without cleanup

Wrong move: Zero-count keys stay in map and break distinct/count constraints.

Usually fails on: Window/map size checks are consistently off by one.

Fix: Delete keys when count reaches zero.

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.