# 「力扣」第 12 题:整数转罗马数字(中等)
# 题目描述
罗马数字包含以下七种字符: I
,V
, X
, L
,C
,D
和 M
。
字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II
,即为两个并列的 1。12 写做 XII
,即为 X + II
。 27 写做 XXVII
, 即为 XX + V + II
。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII
,而是 IV
。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I
可以放在V
(5) 和X
(10) 的左边,来表示 4 和 9。X
可以放在L
(50) 和C
(100) 的左边,来表示 40 和 90。C
可以放在D
(500) 和M
(1000) 的左边,来表示 400 和 900。
给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。
示例 1:
输入: 3
输出: "III"
示例 2:
输入: 4
输出: "IV"
示例 3:
输入: 9
输出: "IX"
示例 4:
输入: 58
输出: "LVIII"
解释: L = 50, V = 5, III = 3
示例 5:
输入: 1994
输出: "MCMXCIV"
解释: M = 1000, CM = 900, XC = 90, IV = 4
1 <= num <= 3999
这道题使用贪心算法的直觉来源是:尽可能优先使用较大数值对应的字符,最后转换得到的罗马数字的字符个数更少,字符更少更方便交流使用,这应该是设计罗马数字的人们的初衷。但是这 其中有一个规则,题目中没有强调,为了避免冲淡主题,我们放在后面说。
# 方法:贪心算法
# 生活中的经验
在以前还使用现金购物的时候,找零钱的时候一般商家会尽量选择面值大的纸币(硬币)给顾客,这样才会使得给顾客的纸币(硬币)张数最少,让顾客点钱的时候更方便。
# 题意分析
本题中,首先给出「罗马数字」与「阿拉伯数字」的对应关系:
罗马数字 | 阿拉伯数字 |
---|---|
I | |
V | |
X | |
L | |
C | |
D | |
M |
先研究几个例子:
阿拉伯数字 | 转换规则 | 罗马数字 |
---|---|---|
直接看表 | I | |
II | ||
III | ||
题目中说的特例:不能写成 | IV | |
直接看表 | V | |
VI | ||
VII | ||
VIII | ||
题目中说的特例:不能写成 | IX | |
直接看表 | X |
# 发现规律
可以发现 规律:数字 IV
。
其实题目中也强调了「做减法的特例」:出现
我们把 所有的 组成罗马数字的最基本的字符组成单元罗列如下,并且按照对应阿拉伯数字降序排序。
罗马数字 | 阿拉伯数字 |
---|---|
M | |
CM | |
D | |
CD | |
C | |
XC | |
L | |
XL | |
X | |
IX | |
V | |
IV | |
I |
设计贪心算法如下:每一步都使用当前对应阿拉伯数字较大的罗马数字作为加法因子,最后得到罗马数字表示就是长度最少的。
贪心算法的证明:
我们先说 题目中隐含的条件,这是我在调试的时候发现的:CM
)、CD
),CMCM
,应该对应 MDCCC
,也就是说,如果能拆分
剩下的可以出现多次的字符有
# 总结
题目中阿拉伯数组转换成罗马数字的规则如下:
、 、 、 、 、 对应的罗马数字字符只出现一次; - 其余字符可以连续出现的次数不超过
次。
按照贪心的方式,尽可能先选出大的数字进行转换。
参考代码:
Java 代码:
public class Solution {
public String intToRoman(int num) {
// 把阿拉伯数字与罗马数字可能出现的所有情况和对应关系,放在两个数组中,并且按照阿拉伯数字的大小降序排列
int[] nums = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] romans = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuilder stringBuilder = new StringBuilder();
int index = 0;
while (index < 13) {
// 特别注意:这里是等号
while (num >= nums[index]) {
stringBuilder.append(romans[index]);
num -= nums[index];
}
index++;
}
return stringBuilder.toString();
}
}
Python 代码:
class Solution:
def intToRoman(self, num: int) -> str:
nums = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
romans = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
index = 0
res = ''
while index < 13:
# 注意:这里是等于号,表示尽量使用大的"面值"
while num >= nums[index]:
res += romans[index]
num -= nums[index]
index += 1
return res
复杂度分析:
- 时间复杂度:
,这里讨论的是一般情况,忽略题目中测试数据范围的限制。输入整数的位数,决定了循环的次数,所以复杂度为 ; - 空间复杂度:
,阿拉伯数字与罗马数字的对应关系也取决于输入数字的位数。
补充说明:
- 一开始我把这个问题想复杂了,觉得需要很复杂的数学证明。直到我把
作为测试数据,后台返回 "MDCCC"
,发现带和 的都是「至多只能使用一次」的字符组合; - 事实上,有一个非常经典的贪心算法的问题,叫「找零钱问题」,就是在找零钱的时候,优先使用大的面值的纸币(或硬币)找给顾客,这样顾客得到的纸币的张数最少。这种贪心选择性质可以成立,是与可以使用的纸币(硬币)的面值有关。例如:
- 可选纸币(硬币)面值列表为
[1, 5, 10]
,要找给顾客元时,给 和 是张数最少的。 - 可选纸币(硬币)面值列表为
[1, 5, 11]
,要找给顾客元时,就不能用贪心算法, 用了 张,最优解为 用了 张。
- 可选纸币(硬币)面值列表为
结论:可以贪心与可选硬币(纸币)的面值有关。
(本题解于 2020 年 11 月 19 日更新)
作者:liweiwei1419 链接:https://suanfa8.com/greedy/solutions/0012-integer-to-roman 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。