LeetCode #13 Easy

Roman to Integer

Convert a Roman numeral string to its integer value.

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].
stringhash-tablemath
Open on LeetCode ↗
02

Intuition

Roman to integer converts a Roman numeral to its value. Roman notation is mostly additive — symbols run largest to smallest and sum — with six subtractive exceptions like IV for 4 and CM for 900. Handling those six as special cases produces long conditional chains. The observation that removes them entirely: - A symbol is subtracted exactly when it is smaller than the symbol immediately after it, and added otherwise. In IV, the I precedes a larger V, so it subtracts. In VI, the I follows and adds. That one comparison covers all six subtractive pairs without naming any of them. So scan left to right, comparing each symbol with its successor, and add or subtract accordingly. The final symbol has no successor and is always added. An equivalent formulation scans right to left, adding when a symbol is at least as large as the previous one seen and subtracting otherwise. Some find this cleaner since it avoids looking ahead. A lookup table maps the seven symbols to their values, and the input is guaranteed valid, so no verification of malformed numerals is needed. The comparison must be strictly less than. Using <= would wrongly subtract in cases like II, where two equal symbols should both add. The constraints cap the input at 3999, so no symbol above M appears. Integer to Roman is the reverse direction, where the subtractive forms are placed in the lookup table as values in their own right — a neat symmetry with this problem's approach of not naming them at all. One pass gives O(n) time and O(1) space.

How to spot this pattern

Roman numerals are additive except when a smaller symbol precedes a larger one, which signals subtraction. So a single left-to-right pass that compares each character to its neighbour handles every case — no lookup table of the six subtractive pairs needed.

03

Approach

Try it first

Before reading on: there are only six subtractive pairs, but special-casing each is fragile. Compare a symbol with the one after it and a single rule falls out — what is it?

1

Find the single rule

A symbol subtracts exactly when it is smaller than the one after it, and adds otherwise. This covers all six subtractive pairs without naming any.

2

Map symbols to values

A seven-entry lookup gives each symbol's value. The input is guaranteed valid, so no malformed-numeral checking is needed.

3

Compare with the successor

Scan left to right, testing each symbol against the next. The final symbol has no successor and is always added.

4

Use strict comparison

Test with <, not <=. Using <= wrongly subtracts in II, where two equal symbols should both add.

5

Or scan right to left

Add when a symbol is at least as large as the previous one seen, subtract otherwise. Equivalent, and avoids looking ahead — some find it cleaner.

6

Rely on the input bound

Values are capped at 3999, so no symbol above M occurs and no overline notation needs handling.

7

Cost of the conversion

One pass with constant work per symbol gives O(n) time and O(1) space for the fixed lookup table.

04

Solution & live demo

▶1class Solution:
▶2 def romanToInt(self, s):
▶3 val = {'I': 1, 'V': 5, 'X': 10, 'L': 50,
▶4 'C': 100, 'D': 500, 'M': 1000}
▶5 total = 0
▶6 for i, ch in enumerate(s):
▶7 if i + 1 < len(s) and val[ch] < val[s[i + 1]]:
▶8 total -= val[ch]
▶9 else:
▶10 total += val[ch]
▶11 return total
05

Common pitfalls

Hardcoding the subtractive pairs

✗ Wrong
s = s.replace('IV', '4').replace('IX', '9')...
✓ Right
if i + 1 < len(s) and val[ch] < val[s[i + 1]]:
    total -= val[ch]

Six special cases is six chances to typo, and the substitution approach breaks if the replacement characters collide with real numerals. The comparison rule generates all six from one principle.

Reading past the end on the last character

✗ Wrong
if val[ch] < val[s[i + 1]]:
✓ Right
if i + 1 < len(s) and val[ch] < val[s[i + 1]]:

The final symbol has no successor, so indexing i + 1 throws. It can never be subtractive anyway — there's nothing after it to be larger.

Using <= for the comparison

✗ Wrong
if val[ch] <= val[s[i + 1]]:
✓ Right
if val[ch] < val[s[i + 1]]:

Equal symbols are additive: "II" is 2, not 0. Only a strictly smaller symbol before a larger one indicates subtraction.

06

Edge cases

Subtractive at the end, e.g. 'IX'

I (1) is less than X (10) so it subtracts; X has no successor so it adds — total 9.

Single symbol, e.g. 'V'

No successor exists, so it is simply added.

Repeated symbols, e.g. 'III'

Each I has an equal (not greater) successor, so all are added — total 3.

07

Complexity

Time
O(n)
Space
O(1)
One pass; the value map is fixed size.