上一题是整数转罗马数字,这一题反过来:给定一个合法的罗马数字,转换成整数。罗马数字里大多数符号是相加的,但当一个小的符号出现在比它大的符号左边时,表示减法(IV 是 4,XC 是 90)。所以只要从左到右扫描,看当前符号是否小于它右边的符号:小于就减去它,否则加上它。本文对 1 到 3999 的全部合法罗马数字做了穷举验证,展示了"直接把每个符号的值相加"这种直觉写法会算出什么,给出五种语言的实现,并把扫描过程做成分步演示。
完整题目见官方页面(链接在文末)。用例是 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
$ 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; } }
$ 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 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; }
$ 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; } };
$ 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 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 3999/3999 Java 通过 3999/3999 Go 通过 3999/3999 JavaScript 通过 3999/3999 C++ 通过 3999/3999
下面的演示用 s = "MCMXCIV"(1994)。每一帧都是把上面的解法在这个输入上真实执行记录下来的,运行器还检查了演示最终得到的整数与 solve 的返回值一致。看 C、X、I 这三个位置:它们各自比右边的符号小,所以是被减掉的。