这一节我们来看 「力扣」第 12 题:整数转罗马数字。
(题目太长,暂时不贴了。)
这道题输入一个整数,输入确保在 1 到 3999 的范围内。输出是罗马数字。题目给了我们一些罗马数字和它们所代表的十进制数值的对应关系。
分析:这道题给我的第一感觉,肯定是罗马数字的字符使用得越少越好。否则表示起来就很不方便。
在这里要注意的是,除了题目中给出的对应关系以外,题目还给了说明:
罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。
还有 6 种特殊的情况,把它们补充进来是这样的:
罗马数字 | 阿拉伯数字 |
---|---|
M | 1000 1000 1000 |
CM | 900 900 900 |
D | 500 500 500 |
CD | 400 400 400 |
C | 100 100 100 |
XC | 90 90 90 |
L | 50 50 50 |
XL | 40 40 40 |
X | 10 10 10 |
IX | 9 9 9 |
V | 5 5 5 |
IV | 4 4 4 |
I | 1 1 1 |
这些数字有一个共同的特点,那就是:
1、如果一个较大的数字,要使用较小的数字的组合时,用到的硬币的个数肯定越多;
2、而且这里给出的数字列表非常全面,是可以凑出任意整数数值的罗马数字的。
能想到用贪心算法,是来源于生活中的经验,表示一个罗马数字,应该使用的数字越少越好。
然后我们尝试使用贪心算法来解决这道问题。
即:
尽量先选择数值大的罗马字母,选择以后就减去这个数字,直到不够减的时候,才考虑下一个数值小的罗马字母。
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();
}
}
发现是可以通过测评的。
在这里是想说明以下求解「贪心算法」问题的经验:
- 先凭感觉,然后尝试举出反例,如果能够举出一个反例,就说明贪心算法不能使用;
- 然后是写代码验证「贪心算法」的有效性。在这里,根据个人情况掌握「贪心算法」的证明。
我对「贪心算法」的证明是这样理解的,就像「时间复杂度」的具体推导过程一样,在初学的时候,不一定要知道得特别具体。
「贪心算法」的证明和「时间复杂度」的具体推导过程,最完备的说明应该是在《算法导论》这本书上。
在《算法导论》第 16 章第 1 节的思考题里提到了「找零问题」,这个问题的描述给出了一些已知的结论。感兴趣的朋友不妨看一看。
「力扣」上其实就有这样的问题,「力扣」的第 322 题:零钱兑换 就是这样的问题,我们可以很容易地举出反例,证明「贪心算法」不能有效工作。而原因在这道题里我们已经和大家介绍过了,和面值相关。
我个人觉得大家了解到这个层面就足以应付找工作过程中的面试和笔试问题了,「贪心选择性质」的证明是不要求掌握的。
在这里,还想和大家分享一个经验,那就是,在使用「贪心算法」之前,一般都需要对输入数组进行排序操作,这样就能达到「局部最优,整体就最优」的效果了。
练习
1、「力扣」第 452 题:用最少数量的箭引爆气球
2、「力扣」第 455 题:分发饼干。