把 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 个整数,本文对全部输入做了穷举验证,而不是只挑几个例子。
完整题目见官方页面(链接在文末)。基准结果由另一个独立的做法给出:按千位、百位、十位、个位分别查表再拼接。题目范围内 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)
$ 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(); } }
$ 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 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; }
$ 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; } };
$ 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 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 3999/3999 Java 通过 3999/3999 Go 通过 3999/3999 JavaScript 通过 3999/3999 C++ 通过 3999/3999
下面的演示用 num = 1994。表中的每一档依次尝试:剩余不小于这个价值就取用(可以取多次),否则跳过。每一帧都是把上面的解法在这个输入上真实执行记录下来的,运行器还检查了演示最终得到的字符串与 solve 的返回值一致。