LeetCode 第 12 题:“整数转罗马数字”题解

题解地址:贪心算法(Python 代码、Java 代码)

说明:文本首发在力扣的题解版块,更新也会在第 1 时间在上面的网站中更新,这篇文章只是上面的文章的一个快照,您可以点击上面的链接看到其他网友对本文的评论。

传送门: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.

贪心算法(Python 代码、Java 代码)

image.png

特别注意:本文介绍的是“贪心算法”,但这种贪心选择性质的成立是有一定条件的。跟这里的“候选”数值有关,如果“候选”数值是另一些数字,“贪心算法”很可能就失效了,此时可能就要应用“动态规划”来解决。这一点在本文并不展开,本人也要进行相应的学习以后,才能解释清楚这个问题。也希望能解释清楚这件事情的朋友能够把您的理解分享给大家。

在生活中的例子

在以前还使用现金购物的年代,如果我们不想让对方找钱,付款的时候我们会尽量拿面值大的纸币给对方,这样才会使得我们给对方的纸币张数最少,对方点钱的时候(因为对方要检验你给的钱对不对)也最方便。最极端的一种情况,你要是都拿零钱去买一个比较贵重的东西,我相信没有人是很高兴收到你的钱的,因为他们点钱费劲。

“整数转罗马数字”与上面的问题是一模一样的思想:在表示一个较大整数的时候,“罗马数字”不会让你都用 11 加起来,肯定是写出来的“罗马数字”的个数越少越好。

于这道问题,“纸币”有哪些,并不是只有题目中给出的对应关系,根据规则,还可以得到一些“纸币”的面值,不过都是有限个“纸币”,很快就能罗列出来。

于是解这道题的思路就出来了:

  • “纸币”有哪些?
  • 一个整数如何做“加法因子”的分解?

思路分析

从题目中给出的“罗马数字”与阿拉伯数字的对应关系,和翻译规则,我们需要推导出“罗马数字”还有哪些组合。

罗马数字 阿拉伯数字
I 11
V 55
X 1010
L 5050
C 100100
D 500500
M 10001000

为此,我们要举例子帮助我们发现规律:

阿拉伯数字 转换规则 罗马数字
11 直接看表 I
22 2=1+12 = 1 + 1,相同数字简单叠加 II
33 3=1+1+13 = 1 + 1 + 1,相同数字简单叠加 III
44 不能写成 4=1+1+1+14 = 1 + 1 + 1 + 144 应该看做 4=514 = 5 - 1 IV
55 直接看表 V
6 6=5+16 = 5 + 1,大数字在前,小数字在后 VI
7 7=5+1+17 = 5 + 1 + 1,大数字在前,小数字在后,相同数字简单叠加 VII
88 8=5+1+1+18 = 5 + 1 + 1 + 1,大数字在前,小数字在后,相同数字简单叠加 VIII
99 不能写成 9=5+1+1+1+19 = 5 + 1 + 1 + 1 + 199 应该看做 9=1019 = 10 - 1 IX
1010 直接看表 X

于是,我们发现(其实在题目中已经强调了这些特例),出现 4499404090904004009009004000400090009000 不讨论了,题目测试用例中有说,不会超过 39993999)的情况比较特殊一些,做的是减法,把它们也加入到“罗马数字”与阿拉伯数字的对应关系表中,并且按照从大到小的顺序排列

罗马数字 阿拉伯数字
M 10001000
CM 900900
D 500500
CD 400400
C 100100
XC 9090
L 5050
XL 4040
X 1010
IX 99
V 55
IV 44
I 11

于是,“将整数转换为阿拉伯数字”的过程,就是我们用上面这张表中右边的数字作为“加法因子”去分解一个整数,并且分解的整数个数越少越好,即尽量使用靠前的数字,这可以认为是一种贪心法则。

参考代码

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

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();
    }
}

复杂度分析

  • 时间复杂度:O(1)O(1),虽然看起来是两层循环,但是外层循环的次数最多 1212,内层循环的此时其实也是有限次的,综合一下,时间复杂度是 O(1)O(1)
  • 空间复杂度:O(1)O(1),这里使用了两个辅助数字,空间都为 1313,还有常数个变量,故空间复杂度是 O(1)O(1)