013 · Roman to Integer

algorithm
Published

May 6, 2026

My First Thoughts

第一眼看这道题,我想到的是:这是不是一个字典匹配游戏?

每个罗马字符都对应一个固定的数字:

在 Python 里,可以先用一个 dictionary 把这些关系存起来:

values = {
    "I": 1,
    "V": 5,
    "X": 10,
    "L": 50,
    "C": 100,
    "D": 500,
    "M": 1000,
}

接下来要判断的是:罗马数字到底是“左大右小”还是“左小右大”。

如果是左大右小,比如:

LVIII

那看起来就可以直接把每个字符对应的值加起来:

50 + 5 + 1 + 1 + 1 = 58

如果是左小右大,比如:

IV

那就不是简单相加,而是要做减法:

5 - 1 = 4

所以最开始我会想先判断整个字符串的方向,再决定怎么处理:

def roman_to_int_first_try(s: str) -> int:
    values = {
        "I": 1,
        "V": 5,
        "X": 10,
        "L": 50,
        "C": 100,
        "D": 500,
        "M": 1000,
    }

    if values[s[0]] > values[s[-1]]:
        left_big_right_small = True
    else:
        left_big_right_small = False

    result = 0

    if left_big_right_small:
        for char in s:
            result += values[char]
    else:
        for char in s[:-1]:
            result -= values[char]
        result += values[s[-1]]

    return result

这个想法存在问题,主要是我对罗马数字的不熟悉,因为罗马数字不一定只有一个整体方向。


Why That Is Not Enough

这个初版想法能处理一些简单情况。

比如 LVIII 是整体左大右小,直接相加没问题。

IV 是整体左小右大,用前面减、最后加的方式也能得到 4

真正的问题出现在这种更混合的数字里:

MCMXCIV

它不是只有一个方向,而是加法和减法关系交替出现:

M C   -> 左大右小
C M   -> 左小右大
M X   -> 左大右小
X C   -> 左小右大
C I   -> 左大右小
I V   -> 左小右大

如果只用 s[0]s[-1] 判断整个字符串的方向:

values[s[0]] > values[s[-1]]

这里会得到:

M > V
1000 > 5

于是程序会认为整个字符串都是“左大右小”,然后把所有字符都加起来:

1000 + 100 + 1000 + 10 + 100 + 1 + 5 = 2216

但正确答案是:

1994

问题不是“方向判断”这个想法错了,而是这个方向不能只判断一次。

罗马数字里的减法关系是局部的。每次看当前字符时,不能只看整个字符串的开头和结尾,而要看它右边的下一个字符。

如果当前值小于右边的值,说明当前字符参与了减法形式,应该减去它。

否则,正常加上它。


Final Idea

从左到右遍历字符串。

对每个位置 i

  • current 表示 s[i] 的值
  • 如果右边还有字符,令 next_value 表示 s[i + 1] 的值
  • 如果 current < next_value,把 current 从答案里减掉
  • 否则,把 current 加到答案里

MCMXCIV 为例:

M    C    M    X    C    I    V
1000 100  1000 10   100  1    5

逐个判断:

M 后面是 C,1000 > 100,加 1000
C 后面是 M,100 < 1000,减 100
M 后面是 X,1000 > 10,加 1000
X 后面是 C,10 < 100,减 10
C 后面是 I,100 > 1,加 100
I 后面是 V,1 < 5,减 1
V 是最后一个,加 5

结果:

1000 - 100 + 1000 - 10 + 100 - 1 + 5 = 1994

Why It Works

合法罗马数字中,减法形式只会表现为一个局部关系:左边的值比右边的值小。

因此不需要单独枚举 IVIXXLXCCDCM

只要在遍历时比较当前字符和下一个字符:

  • 当前值小于下一个值时,当前值应该被减去
  • 否则,当前值应该被加上

这样每个字符只处理一次,所有普通加法和特殊减法都能被同一个规则覆盖。


Code

def roman_to_int(s: str) -> int:
    values = {
        "I": 1,
        "V": 5,
        "X": 10,
        "L": 50,
        "C": 100,
        "D": 500,
        "M": 1000,
    }

    total = 0

    for i in range(len(s)):
        current = values[s[i]]

        if i + 1 < len(s) and current < values[s[i + 1]]:
            total -= current
        else:
            total += current

    return total

Complexity

Time \(O(n)\) — 每个字符只遍历一次
Space \(O(1)\) — 映射表大小固定

Takeaway

当一个规则看起来有很多特殊情况时,先找它们共同的局部特征。这道题里,所有减法形式都可以归纳成同一件事:当前值小于右边的值时,当前值要被减掉。


Quiz