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

LeetCode 12. 整数转罗马数字

把 1 到 3999 的整数转换成罗马数字。罗马数字用 I V X L C D M 七个符号表示 1、5、10、50、100、500、1000,通常大的符号写在左边、做加法;但有六种"减法组合"——IV(4)、IX(9)、XL(40)、XC(90)、CD(400)、CM(900)——小的符号写在大的符号左边,表示大减小。这道题的关键是把这六个减法组合也放进价值表里,然后从大到小贪心:能减就减。因为输入范围只有 3999 个整数,本文对全部输入做了穷举验证,而不是只挑几个例子。

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

题意与示例

完整题目见官方页面(链接在文末)。基准结果由另一个独立的做法给出:按千位、百位、十位、个位分别查表再拼接。题目范围内 1 到 3999 的全部 3999 个整数都作为用例,前几个如下:

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

陷阱:只用七个基本符号做加法

最容易想到的做法是只用七个符号,从大到小不停地减。这样得到的结果数值上是对的,但不是标准写法,因为它没有使用减法组合。下面是这种写法和标准写法在几个数上的真实对比:

真实实测 · 本机执行输出
# 只用 7 个基本符号做加法(没有 CM、CD、XC、XL、IX、IV 这些减法组合)
   4: 只做加法 -> IIII               solve -> IV           是否相同:False
   9: 只做加法 -> VIIII              solve -> IX           是否相同:False
  58: 只做加法 -> LVIII              solve -> LVIII        是否相同:True
1994: 只做加法 -> MDCCCCLXXXXIIII    solve -> MCMXCIV      是否相同:False
3999: 只做加法 -> MMMDCCCCLXXXXVIIII solve -> MMMCMXCIX    是否相同:False

4 应该写 IV,只做加法会写成 IIII;1994 应该是 MCMXCIV,只做加法是 MDCCCCLXXXXIIII。58 这类不含减法组合的数,两种写法碰巧相同,所以只测这样的例子发现不了问题。把六个减法组合加进从大到小的价值表里就能解决。

贪心:五种语言的实现

价值表是 13 项,从 1000 到 1,其中包含六个减法组合。从前往后依次处理:只要剩余的数不小于当前价值,就写出对应符号并减去这个价值。因为每一档都用尽了才会往下走,结果一定是标准写法。五种语言都是真实编译运行的。

Python · 解法代码def solve(num): table = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")] out = [] for value, symbol in table: # 从大到小,能减就减:贪心 while num >= value: out.append(symbol) num -= value return "".join(out)
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[1]  →  "I"
[2]  →  "II"
[3]  →  "III"
[4]  →  "IV"
[5]  →  "V"
[6]  →  "VI"
Java · 解法代码class Solution { public String intToRoman(int num) { int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; StringBuilder out = new StringBuilder(); for (int k = 0; k < values.length; k++) { // 从大到小,能减就减:贪心 while (num >= values[k]) { out.append(symbols[k]); num -= values[k]; } } return out.toString(); } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[1]  →  "I"
[2]  →  "II"
[3]  →  "III"
[4]  →  "IV"
[5]  →  "V"
[6]  →  "VI"
Go · 解法代码func intToRoman(num int) string { values := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1} symbols := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"} var out strings.Builder for k, v := range values { // 从大到小,能减就减:贪心 for num >= v { out.WriteString(symbols[k]) num -= v } } return out.String() }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[1]  →  "I"
[2]  →  "II"
[3]  →  "III"
[4]  →  "IV"
[5]  →  "V"
[6]  →  "VI"
JavaScript · 解法代码function intToRoman(num) { const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]; const symbols = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']; let out = ''; for (let k = 0; k < values.length; k++) { // 从大到小,能减就减:贪心 while (num >= values[k]) { out += symbols[k]; num -= values[k]; } } return out; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[1]  →  "I"
[2]  →  "II"
[3]  →  "III"
[4]  →  "IV"
[5]  →  "V"
[6]  →  "VI"
C++ · 解法代码#include <string> using namespace std; class Solution { public: string intToRoman(int num) { static const int values[] = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; static const char* symbols[] = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; string out; for (int k = 0; k < 13; k++) { // 从大到小,能减就减:贪心 while (num >= values[k]) { out += symbols[k]; num -= values[k]; } } return out; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[1]  →  "I"
[2]  →  "II"
[3]  →  "III"
[4]  →  "IV"
[5]  →  "V"
[6]  →  "VI"

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

五种语言对全部 3999 个输入的输出,与基准解逐个比对,结果如下(这不是抽样测试,输入范围内没有遗漏):

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

分步演示:贪心怎么逐档消耗

下面的演示用 num = 1994。表中的每一档依次尝试:剩余不小于这个价值就取用(可以取多次),否则跳过。每一帧都是把上面的解法在这个输入上真实执行记录下来的,运行器还检查了演示最终得到的字符串与 solve 的返回值一致。

整数转罗马数字:num = 1994
当前尝试的价值本次取用

    参考与说明

    • 官方题目页:LeetCode 12. 整数转罗马数字。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。这道题的输入空间很小,所以做的是穷举而不是随机对拍。
    • 没有覆盖:大于等于 4000 的数(罗马数字有另外的写法,题目也不要求);反方向的转换见下一题。
    京ICP备2026067502号-1京公网安备11010502063487号