A film archive stored copyright years as Roman strings on the slate. The ingest job replaced IV, IX, XL, XC, CD, and CM with additive runs, then summed every letter. A weekend of title cards returned in seconds. A century of reels allocated a rewritten string for every row while the catalog window closed.

Roman to Integer asks for the numeric value of a valid Roman numeral string. Each character already has a fixed value. Expanding the six subtractive pairs uses that and still pays six rewrites. One peek at the next value is enough: if the current numeral is smaller, subtract it; otherwise add.

This is an interview writeup, not a grammar of Roman numerals. Here we only care about when a smaller numeral sits before a larger one so the total is a signed walk, not a nested replace.

The problem

Given a string s of Roman numerals, return its integer value. Letters map I=1, V=5, X=10, L=50, C=100, D=500, M=1000. The six subtractive pairs are IV=4, IX=9, XL=40, XC=90, CD=400, CM=900. Assume s is a valid Roman numeral.

s = "III"      →  3
s = "LVIII"    →  58      L + V + III
s = "MCMXCIV"  →  1994    M + CM + XC + IV

Note: Replace the six pairs, then sum, is a legal middle path. It is not the usual interview answer: the prompt did not ask you to rewrite the string, and one comparison with the next value answers in a single pass.

Replace the pairs, then sum, is the honest brute force

Expand each subtractive pair into additive form, then add every letter. IV becomes IIII, CM becomes DCCCC. Correct. Extra linear space for the rewritten string. A nest of startsWith("IV") ifs is the same idea with the pairs hardcoded.

int romanToIntReplace(String s) {
    s = s.replace("CM", "DCCCC")
         .replace("CD", "CCCC")
         .replace("XC", "LXXXX")
         .replace("XL", "XXXX")
         .replace("IX", "VIIII")
         .replace("IV", "IIII");
    Map<Character, Integer> val = Map.of(
        'I', 1, 'V', 5, 'X', 10, 'L', 50,
        'C', 100, 'D', 500, 'M', 1000
    );
    int total = 0;
    for (int i = 0; i < s.length(); i++) {
        total += val.get(s.charAt(i));
    }
    return total;
}

At a copyright year this is a rounding error. At a backfill of the archive you paid six copies for a question a peek answers in place: is the current value smaller than the next?

One pass: subtract when the next numeral is larger

Map each letter to its value. Walk left to right. Let cur be the value of s.charAt(i).

  • If there is a next letter and cur is smaller, subtract cur.
  • Otherwise add cur.

You never rewrite s. You never skip an index after a subtract: IV is -1 then +5. The last letter has no next, so it is always added.

Walk MCMXCIV:

s = "MCMXCIV"

i=0  M=1000  next C=100   1000>=100  +1000   total=1000
i=1  C=100   next M=1000  100<1000   -100    total=900
i=2  M=1000  next X=10    1000>=10   +1000   total=1900
i=3  X=10    next C=100   10<100     -10     total=1890
i=4  C=100   next I=1     100>=1     +100    total=1990
i=5  I=1     next V=5     1<5        -1      total=1989
i=6  V=5     last                    +5      total=1994

M + CM + XC + IV is 1000 + 900 + 90 + 4. The Java is that walk:

int romanToInt(String s) {
    Map<Character, Integer> val = Map.of(
        'I', 1, 'V', 5, 'X', 10, 'L', 50,
        'C', 100, 'D', 500, 'M', 1000
    );
    int total = 0;
    int n = s.length();
    for (int i = 0; i < n; i++) {
        int cur = val.get(s.charAt(i));
        if (i + 1 < n && cur < val.get(s.charAt(i + 1))) {
            total -= cur;
        } else {
            total += cur;
        }
    }
    return total;
}

Time is O(n) — one pass, constant-time lookup per index. Space is O(1) for the seven-letter map.

Note: Do not skip the next index after a subtract. Mixing “treat IV as one token of 4” with “sign the current letter” drops V or double-counts it. Pick one proof. The peek-and-sign proof visits every index once.

What interviewers usually poke next

  • Right to left. Remember the previous (already processed, right-hand) value. If current is smaller, subtract; else add. Same O(n), no peek at i + 1.
  • A 128-slot array instead of a map. val['M'] = 1000 is the same lookup without boxing. Say that if they flinch at Map.of.
  • Integer to Roman. Greedy from M down through the six pairs. Different prompt; this walk does not emit letters.
  • Invalid or empty. Constraints usually give a valid numeral of length at least 1. At the board, ask. int is enough: the largest standard value is 3999.

You are done with this problem when you can say, out loud, why expanding IV into IIII is correct, why a smaller-before-larger pair is a minus on the current letter, and why you must not skip the next index after that minus.