bigywater.tech
算法与数据结构 · LeetCode 13

LeetCode 13. 罗马数字转整数

上一题是整数转罗马数字,这一题反过来:给定一个合法的罗马数字,转换成整数。罗马数字里大多数符号是相加的,但当一个小的符号出现在比它大的符号左边时,表示减法(IV 是 4,XC 是 90)。所以只要从左到右扫描,看当前符号是否小于它右边的符号:小于就减去它,否则加上它。本文对 1 到 3999 的全部合法罗马数字做了穷举验证,展示了"直接把每个符号的值相加"这种直觉写法会算出什么,给出五种语言的实现,并把扫描过程做成分步演示。

3999
组用例(含边界)与暴力解的结果一致
9 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。用例是 1 到 3999 每个整数对应的标准罗马数字(用查表法生成),一共 3999 个,前几个如下。基准解是另一个独立的做法:先把六个减法组合展开成纯加法(IV 变成 IIII 等),再把每个符号的值直接相加。为了保证基准本身可靠,脚本在加载时断言了:对这 3999 个罗马数字,基准解算出的整数都恰好等于生成它的那个整数:

真实实测 · 本机执行输出
输入 ["I"]
输出 1
输入 ["II"]
输出 2
输入 ["III"]
输出 3
输入 ["IV"]
输出 4
输入 ["V"]
输出 5
输入 ["VI"]
输出 6

陷阱:直接把每个符号的值相加

如果忘了减法组合,把每个符号的值直接累加,遇到 IV、IX、XC、CM 等就会多算:

真实实测 · 本机执行输出
# 把每个符号的值直接相加(不识别减法组合)
III     : 直接相加 ->     3   solve ->     3   暴力解 ->     3
IV      : 直接相加 ->     6   solve ->     4   暴力解 ->     4
IX      : 直接相加 ->    11   solve ->     9   暴力解 ->     9
LVIII   : 直接相加 ->    58   solve ->    58   暴力解 ->    58
MCMXCIV : 直接相加 ->  2216   solve ->  1994   暴力解 ->  1994

IV 应该是 4,直接相加得到 6(I 的 1 加上 V 的 5,而不是 V 减去 I);MCMXCIV 应该是 1994,直接相加得到 2216。III、LVIII 这类只含加法的数两种写法恰好相同,所以只测这样的输入发现不了问题。

从左到右扫描:五种语言的实现

对每个位置,取出当前符号的值 v:如果右边还有符号,并且 v 小于右边符号的值,就减去 v(这是减法组合的左半部分),否则加上 v。Python 和 JavaScript 用字典,Go 用 map[byte]int,Java 和 C++ 用 switch。五种语言都是真实编译运行的。

Python · 解法代码def solve(s): value = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} total = 0 for i, ch in enumerate(s): v = value[ch] if i + 1 < len(s) and v < value[s[i + 1]]: # 小的符号在大的符号左边:这是一个减法组合 total -= v else: total += v return total
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
["I"]  →  1
["II"]  →  2
["III"]  →  3
["IV"]  →  4
["V"]  →  5
["VI"]  →  6
Java · 解法代码class Solution { private int value(char c) { switch (c) { case 'I': return 1; case 'V': return 5; case 'X': return 10; case 'L': return 50; case 'C': return 100; case 'D': return 500; default: return 1000; // 'M' } } public int romanToInt(String s) { int total = 0; for (int i = 0; i < s.length(); i++) { int v = value(s.charAt(i)); if (i + 1 < s.length() && v < value(s.charAt(i + 1))) total -= v; // 小的在大的左边:减法组合 else total += v; } return total; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
["I"]  →  1
["II"]  →  2
["III"]  →  3
["IV"]  →  4
["V"]  →  5
["VI"]  →  6
Go · 解法代码func romanToInt(s string) int { value := map[byte]int{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} total := 0 for i := 0; i < len(s); i++ { v := value[s[i]] if i+1 < len(s) && v < value[s[i+1]] { // 小的在大的左边:减法组合 total -= v } else { total += v } } return total }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
["I"]  →  1
["II"]  →  2
["III"]  →  3
["IV"]  →  4
["V"]  →  5
["VI"]  →  6
JavaScript · 解法代码function romanToInt(s) { const value = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; let total = 0; for (let i = 0; i < s.length; i++) { const v = value[s[i]]; if (i + 1 < s.length && v < value[s[i + 1]]) total -= v; // 小的在大的左边:减法组合 else total += v; } return total; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
["I"]  →  1
["II"]  →  2
["III"]  →  3
["IV"]  →  4
["V"]  →  5
["VI"]  →  6
C++ · 解法代码#include <string> using namespace std; class Solution { int value(char c) { switch (c) { case 'I': return 1; case 'V': return 5; case 'X': return 10; case 'L': return 50; case 'C': return 100; case 'D': return 500; default: return 1000; // 'M' } } public: int romanToInt(string s) { int total = 0; for (int i = 0; i < (int)s.size(); i++) { int v = value(s[i]); if (i + 1 < (int)s.size() && v < value(s[i + 1])) total -= v; // 小的在大的左边:减法组合 else total += v; } return total; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
["I"]  →  1
["II"]  →  2
["III"]  →  3
["IV"]  →  4
["V"]  →  5
["VI"]  →  6

穷举验证:全部 3999 个输入逐一对比

五种语言对全部 3999 个罗马数字的输出,与基准解逐个比对,结果如下:

真实实测 · 本机执行输出
共 3999 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 3999/3999
  Java        通过 3999/3999
  Go          通过 3999/3999
  JavaScript  通过 3999/3999
  C++         通过 3999/3999

分步演示:符号逐个扫描

下面的演示用 s = "MCMXCIV"(1994)。每一帧都是把上面的解法在这个输入上真实执行记录下来的,运行器还检查了演示最终得到的整数与 solve 的返回值一致。看 C、X、I 这三个位置:它们各自比右边的符号小,所以是被减掉的。

罗马数字转整数:s = "MCMXCIV"
当前符号已处理

    参考与说明

    • 官方题目页:LeetCode 13. 罗马数字转整数。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。这道题的合法输入只有 3999 个,所以做的是穷举而不是随机对拍。
    • 没有覆盖:不合法的罗马数字(如 IIII、VX)——题目保证输入合法,本文的实现对这样的输入不做任何保证。
    京ICP备2026067502号-1京公网安备11010502063487号