动态规划面试宝典
卢誉声
Autodesk 首席工程师
9629 人已学习
新⼈⾸单¥59
登录后,你可以任选2讲全文学习
课程目录
已完结/共 23 讲
动态规划面试宝典
15
15
1.0x
00:00/00:00
登录|注册

01 | 硬币找零问题:从贪心算法说起

你好,我是卢誉声。
作为“初识动态规划”模块的第一节课,我会带着你一起从贪心算法开始了解整个知识体系的脉络。现实中,我们往往不愿意承认自己贪婪。事实上,贪婪是渴望而不知满足,它是人的一种基本驱动力。既然是基本驱动力,那它自然就不会太难。
所以你可能会说贪心算法很简单啊,但其实不然,这里面还真有不少门道值得我们说说。而且,它还跟动态规划问题有着千丝万缕的联系,能够帮助我们理解真正的动归问题。
接下来我们就从一个简单的算法问题开始探讨,那就是硬币找零。在开始前,我先提出一个问题:任何算法都有它的局限性,贪心算法也如此,那么贪心算法能解决哪些问题呢?
你不妨带着这个问题来学习下面的内容。

硬币找零问题

移动支付已经成为了我们日常生活当中的主流支付方式,无论是在便利店购买一瓶水,还是在超市或菜市场购买瓜果蔬菜等生活用品,无处不在的二维码让我们的支付操作变得异常便捷。
但在移动支付成为主流支付方式之前,我们常常需要面对一个简单问题,就是找零的问题。
虽然说硬币找零在日常生活中越来越少,但它仍然活跃在编程领域和面试问题当中,主要还是因为它极具代表性,也能多方面考察一个开发人员或面试者解决问题的能力。
既然如此,我们就先来看看这个算法问题的具体描述。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

贪心算法是一种简单而有效的解决算法问题的方法,本文以硬币找零问题为例,介绍了贪心算法的基本思路和应用。文章首先强调了贪心算法的局部最优解和整体最优解的区别,然后通过具体问题的讲解,阐述了贪心算法在解决最值问题时的应用。贪心算法的局限性也在文章中得到了讨论,指出了纯粹采用贪心策略可能无法达到整体最优的问题。然而,贪心算法作为求解整体最优的思路源头,为其他优化方法提供了基础。在没有优化的情况下,穷举从来就不算是一个好方法。所以我带你使用了贪心算法来解题,它是一种使用局部最优思想解题的算法。但是通过硬币找零问题,我们也发现了贪心算法本身的局限性。在求解最值问题时,我们需要更好的方法来解。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《动态规划面试宝典》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(57)

  • 最新
  • 精选
  • AshinInfo
    置顶
    重新调整得了java代码部分,提高代码的可读性 private static void getMinCoinCountOfValue() { // 硬币面值 int[] values = {5, 3}; // 总价 int total = 11; int minCoinCount = getMinCoinCountOfValueHelper(total, values); // 输出结果 System.out.println(minCoinCount); } /** * @param total 金额 * @param coins 币种数组,从大到小排序 * @return 返回币数,如果返回-1表示无法凑够total */ private static int getMinCoinCountOfValueHelper(int total, int[] coins) { if (coins.length == 0) { return -1; } //当前币值 int currentCoin = coins[0]; //使用当前币值数量 int useCurrentCoinCount = total / currentCoin; int restTotal = total - useCurrentCoinCount * currentCoin; // 如果restTotal为0,表示余额已除尽,组合完成 if (restTotal == 0) { return useCurrentCoinCount; } // 其他币种数量 int coninCount = -1; // 剩余的币种 int[] restCoins = Arrays.copyOfRange(coins, 1, coins.length); while (useCurrentCoinCount >= 0) { // 否则尝试用剩余面值求当前余额的硬币总数 coninCount = getMinCoinCountOfValueHelper(restTotal, restCoins); // 如果后续没有有可用组合,退一步,当前useCurrentCoinCount币数减1 if (coninCount == -1) { // 否则尝试把当前面值数-1 useCurrentCoinCount--; // 重新计算restTotal restTotal = total - useCurrentCoinCount * currentCoin; } else { return useCurrentCoinCount + coninCount; } } return -1; }

    作者回复: 赞,感谢你的分享,顶一顶,让更多人看到。

    2020-09-16
    12
    65
  • sanyinchen
    上述回溯+贪心并不能取到最优解, 比如[1,7,10] amount=14 那么根据递归深搜 10 + 1 + 1 + 1 + 1 会比 7 + 7 先到

    作者回复: 嗯,这里的代码逻辑需要再加上“回溯时比较保留最小值”,这样就没有问题了。 贪心真的很容易过于贪婪,代码已更新。

    2020-09-16
    3
    27
  • 梅亮宏@创造力
    老师说的很生动!用递归加局部最优的方法一定能得到正解。但是如果问题变得更加复杂的情况下,例如我们有1亿中硬币可以用,总币值为几万亿。可能还需要优化一下算法性能或者用分布式计算把性能提高? 这让我想到了ai中的reinforcement learning。个人认为有些偏全局优化?就如alphaGo,每一步棋都需要以整个棋局为出发点考虑。有点像老师这个算法里面的递归,即每尝试走一步的时候都会记录状态,用树状结构罗列所有可能性然后回溯。通过神经网络来的weight值估算是不是应该回溯。另外即使找到最优解了,还可以设置exploit更多最优的可能。纯个人理解,有点发散,希望老师指正!

    作者回复: 没错,寻找最优解的时候,本质上还是需要进行枚举(穷举),因此优化算法性能是最重要的第一步,如果使用了动态规划,就可以大幅压缩计算时间,在此基础上,如果性能仍不满意(即数据规模实在太巨大),那么则可以考虑使用工程化的方法,比如你提到的分布式计算来提高计算性能。 AI 中的强化学习,本质上它的目的是寻找全局最优解。但是,我们最终能找的只是一定范围内的局部最优解,使得这个局部最优解在一定条件下,最接近我们期望的全局最优解。这个发散点很好,算法发展到一定高度就会进入机器学习领域,无论如何,基本的算法思路其实没有本质差别。

    2020-09-15
    2
    10
  • AshinInfo
    递归的目的是求解 回溯+递归的目的是枚举所有组合的解,并取最优解返回 没有回溯,递归只能获得一个解或者无解,获得的解不一定是最优解 递归是一种算法结构,回溯是一种算法思想 一般回溯多用递归实现

    作者回复: 是的,回溯就相当于穷举过程中递归的一个补丁(patch)。 另外,你对递归和回溯的理解、比喻,比较恰当。可以这么理解。

    2020-09-27
    8
  • Karl
    老师,第一段代码的第22行,是不是应该为调用GetMinCoinCountHelper?

    作者回复: 你好 Karl! 感谢你提出此问题。代码已经做了相应调整和更新。并追加了 Java 版本的代码供大家参考。

    2020-09-14
    6
  • 好运来
    测试在原有贪心基础上加上回溯可以找到一组可行解: int[] values = new int[] {5, 3}; // 硬币面值 int total = 11; // 总价 贪心策略求出可行解不是全局最优解: values = new int[]{5, 4, 1}; // 硬币面值 total = 13; // 总价

    作者回复: 赞。事实上,纯粹的贪心算法只考虑了局部最优的情况,因此在绝大多数情况下是得不到最优解的。在回溯版本中,需要枚举所有的组合,并保留最小的结果。本质上,这里是通过回溯来进行穷举的,因此效率还是不够好。后面的课程会给出更好的解决方法。

    2020-09-14
    4
  • EncodedStar
    对动规有了新的了解,感谢老师!

    作者回复: 欢迎进入动归世界。 动态规划其实也不难,是有规可循的。跟随课程,我们一起进步。

    2020-09-14
    2
    4
  • vin
    GetMinCoinCountLoop这个方法是在干嘛呀?感觉直接getMinCoinCountOfValue(total, values, 0)不就行了吗?GetMinCoinCountLoop将数组元素换来换去有啥意义吗?有点看不懂,要是多加点注释就好了

    作者回复: 这里实际上是考虑所有的排列组合。

    2021-08-18
    3
  • 020
    看来半天回溯+贪心,发现其实就是把所有情况都枚举出来取最小值。。。。。。

    作者回复: 贪心本身其实是不想枚举出所有情况,不过这个就会导致只能得到局部最优解,最后想要解决问题还是要通过不同方式枚举出所有组合。

    2021-05-19
    3
  • 宋不肥
    时隔1个多月,二刷一下,发现这一篇比第二篇代码难读,就在于变量和函数的命名,实在是又长,意义又差,比如valueCount 完全不如valuelength这样的名字来的直观,而且取名还很像,很多时候看了几行,就忘记标识符号的意思是啥来着了,第二篇命名改进了之后可读性就好很多。

    作者回复: 谢谢建议,变量命名可读性这个因人而异,的确不能满足所有人的命名习惯,我会考虑改进的。

    2021-01-27
    2
收起评论
显示
设置
留言
57
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部