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

04 | 动态规划:完美解决硬币找零

你好,我是卢誉声。今天我们来继续学习动态规划。
在前面的几节课中,我们经历了贪心算法求解硬币找零的问题,并从中发现了贪心算法本身的局限性:它几乎只考虑了局部最优,因此无法应对需要考虑整体最优的算法面试问题。
针对这一问题,我们重新思考了解决方案,用递归的方法来穷举出所有可能的组合,从这些可能组合中找出最优解。虽然这么做考虑了整体最优,而且真的可以解决问题,但效率太低。因此,为了解决这个低效问题,我们又提出了备忘录的概念,并在硬币找零案例中应用它解决了问题。
你应该发现了,我们在解决硬币找零问题时的思路是一以贯之的:发现问题,找解决方案;如果方案有局限性,那么就看如何扩展视野,找寻更优的方法。
不知道你还记不记得,我在上一节课的结尾有提到:含有备忘录的递归算法已经与动态规划思想十分相似了,从效率上说也是如此。
事实上,你已经在使用动态规划的思想解决问题了。但“真正”的动态规划解法跟备忘录法又有什么区别呢?
接下来我们就带着这个问题,一起来学习今天的内容吧!

动态规划的问题描述

我们曾不止一次提到重叠子问题,并在上一课对其做了深入探讨。其实,重叠子问题是考虑一个问题是否为动态规划问题的先决条件,除此之外,我还提到了无后效性。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了动态规划在解决硬币找零问题中的应用,并提供了通用的动态规划框架。从贪心算法到动态规划的演进过程进行了详细分析,强调了动态规划的重要性和优势。文章指出了动态规划问题的核心是正确的状态转移方程,以及如何通过自底向上的备忘录消除重叠子问题,构造DP table。此外,还介绍了带备忘录的递归解法和动态规划之间的关系,以及动态规划的状态转移方程的构造方法。最后,通过一个具体问题引出了动态规划的思想和应用,为读者提供了清晰的技术概念和解决问题的方法。整体而言,本文内容深入浅出,适合读者快速了解动态规划的基本概念和应用。

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

全部留言(25)

  • 最新
  • 精选
  • R20114
    课后思考题, class Solution { // 状态定义: dp[i] 表示截止到 nums 中 i 下标, 最大子数组和 // 状态转移: dp[i] = max(dp[i - 1] + nums[i], nums[i]) // 截止到数组的第 i 个下标, 子数组和的构成是下面两种情况结果中较大的值: // 1. 由截止到 i - 1 下标的最大子数组和与 nums[i] 构成 --> dp[i - 1] + nums[i] // 2. nums[i] // 结果是 max(dp[0]...dp[nums.length - 1]) public int maxSubArray(int[] nums) { if (nums == null) throw new IllegalArgumentException("nums can not be null!"); if (nums.length == 0) return 0; int[] dp = new int[nums.length]; dp[0] = nums[0]; int max = dp[0]; for (int i = 1; i < nums.length; i++) { dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); max = Math.max(dp[i], max); } return max; } } 时间复杂度: O(n) 空间复杂度: O(n) 空间优化: 在状态转移的过程中, 使用变量记录前一次状态和当前状态 class Solution { public int maxSubArray(int[] nums) { if (nums == null) throw new IllegalArgumentException("nums can not be null!"); if (nums.length == 0) return 0; int p = nums[0]; int max = p; for (int i = 1; i < nums.length; i++) { int t = Math.max(p + nums[i], nums[i]); max = Math.max(t, max); p = t; } return max; } }

    作者回复: 赞。顶上去!空间优化的思路是正确的。

    2020-09-25
    2
    13
  • webmin
    最大和的连续子数组,一个数加正数会变大,加负数时会变小,关键的是正负是无规律出现的,且遇到负数也不一定就不能得到最大和的连续子数组(如:课后思考中的例子),通过观察我们可以发现当 sum(nums[0...i-1]) < nums[i]我们就选择nums[i]重新开始累加就好( [-2, 1, -3, 1] -2+1+-3 < 1),但是类似的这样区间可能会出现好多个,所以还需要一个变量来保存和比较历次区间和,最后此值就是最大和。 ```java public int maxSubArray(int[] nums) { int[] dp = new int[nums.length]; int maxSum = nums[0]; dp[0] = nums[0]; for(int i = 1; i < nums.length; i++){ //因为只需要记住前一个状态,所以这里是可以优化为一个变量来取代dp数组,这里是为了和老师课程dp示范呼应 dp[i] = Math.max(dp[i-1] + nums[i],nums[i]); maxSum = Math.max(maxSum,dp[i]); } return maxSum; } ```

    作者回复: 赞!

    2020-09-21
    2
    5
  • 追风筝的人
    老师 是不是自底向上的迭代处理思想 就是 动态规划的解决思路?

    作者回复: 差不多可以这么说。这是因为动态规划思想的核心就是用小问题的最优解去推导更大问题的最优解,最终通过组合子问题的最优解得到原问题的答案,自底向上很好的顺应了这一思想。同时,自底向上推导也符合状态转移方程的定义。所以这么说是恰到好处的。

    2020-10-20
    2
    4
  • 帽子狗
    这也太详细了.. 硬是三章引出来方程。 老师太贴心了。

    作者回复: 感谢你的肯定。事实上动态规划的提出正是从贪心算法开始的,为了让大家能够深刻理解动态规划的由来,我们有必要从贪心算法开始,再到穷举,最后再到动态规划。理解了其本质,才能说真正理解了动态规划。 不然,刷了再多题,看了再多讲解,也是空中楼阁,抓不住问题的根源,很难真正掌握这门手艺。

    2020-10-19
    3
  • AshinInfo
    在动态规划中,我们将其称之为状态参数。同时,你应该注意到了,这个状态在不断逼近初始化状态。而这个不断逼近的过程,叫做状态转移。 这里的逼近初始状态,是自顶向下,这样说可以理解 但是如果是动态规划的话,是否这样说说比较好点 从初始状态不断的逼近目标状态

    作者回复: 你理解的很透彻! 事实上,动态规划解法都是从小问题推广到大问题的,也就是你说的自底向上。虽然这里所说的是让计算逼近初始化状态,但其实真正的计算方向,是反过来的。

    2020-09-29
    3
  • 宋不肥
    自底向上递推,其实就是每一步都用贪心思想来向前贪心,向前贪心的好处是站在结果向前贪心,得到的就是之前各个最优解的选择,在这些最优解中必然包含了全局最优解,所以利用贪心可以得到全局最优解。第一步范围很小,贪心的局部最优就是全局最优。由数学归纳法,这样利用贪心往后递推,每一步向前贪心,每一步都是全局最优解,自底向上还省去了递归方法中判断多余分支的时间成本和开堆栈的空间成本(但其实现在计算机体系优化的很好了,个人感觉不是特别大的数据,这部分空间优化的开销感觉可以忽略).

    作者回复: 对的。DP里肯定是用到了和贪心类似的“根据当前情况“求解”下一步的最优解“的思路。只不过DP是在分析问题的性质和结构后定义了一个正确的”贪心“函数,确保每一步的贪心(局部最优解)得到的就是这种状况下的全局最优解,只不过求解顺序肯定是自下向上而已。同时又利用缓存避免重复计算。 其实这里自底向上主要省去的还是时间成本。

    2020-10-28
    2
  • Roy Liang
    int main() { int n, i, maxn = 0; int a[10000], b[10000]; cin >> n; for (i = 0; i < n; i++) { cin >> a[i]; maxn = min(a[i], maxn); } b[0] = a[0]; for (i = 1; i < n; i++) { b[i] = b[i-1] >= 0 ? b[i-1] + a[i] : a[i]; maxn = max(b[i], maxn); } cout << maxn << endl; }

    作者回复: 没有问题。赞。

    2020-10-18
    1
  • fatty Jack
    //写了最基本的DP,参考了下大牛的代码,就是不一样 var maxSubArray = function(nums) { let ret = nums[0] //使用上一次的nums[i]存储dp状态 for (let i = 1; i < nums.length; i++) { //通过Math.max(nums[i - 1], 0)不需要区分大于/小于0的情况 nums[i] += Math.max(nums[i - 1], 0) ret = Math.max(ret, nums[i]) } return ret };

    作者回复: 这个解法没问题,但是有一个建议,即需要注意在解题的时候,最好不要就地修改输入数组的内容。这样做对性能损耗比较大。

    2020-09-22
    1
  • Monroe He
    # python version 3.7 import copy def max_sum_helper(values): # 初始化状态, 为其自身值 dp = copy.deepcopy(values) # 状态转移(参数变化) 改变状态表 values_length = len(values) for i in range(1,values_length): # 决策 tmp = values[i] if values[i]>=values[i]+dp[i-1] else values[i]+dp[i-1] dp[i] = tmp return max(dp) def get_max_sum(): # 数组 values = [-2, 1, -3, 1, -1, 6, 2, -5, 4] return max_sum_helper(values) # 输出答案 get_max_sum()

    作者回复: 顶一下,让其他人也能看到

    2022-11-08归属地:北京
  • 😁
    「把备忘录中剩余的位置初始化成 k + 1」:这里不是初始化为-1的吗

    作者回复: 初始化为-1,是前面采用比较容易理解的递归+备忘录直接实现的时候。下面使用循环的动态规划中初始化的值是k+1。之所以不初始化为-1是因为后面需要取min,用k+1可以代表无穷大。

    2022-06-29
收起评论
显示
设置
留言
25
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部