都说这种类型的题是最难的,我也有点惧怕,不过还是要迎难而上。

动态规划问题的一般形式就是求最值,求最值的核心就是穷举,暴力穷举会导致效率低下哎,所以我们需要一个备忘录来优化穷举过程。

而且,动态规划问题一定具备最优子结构,这样才能通过子问题的最值得到原问题的最值

动态规划三要素是重叠子问题(备忘录优化),最优子结构,状态转移方程,最难的一步就是构建出正确的状态转移方程。

动态规划采用自底向上的推理方法,从规模最小的一直推到规模最大的,所以动态规划一般都脱离了递归,采用循环迭代完成计算。

有了这些关于动态规划的了解后,我们来实战一下

凑零钱问题

题目描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

题目分析:

求最值问题,又有最优子结构,可以立马想到动态规划。

最优子结构即子问题间必须相互独立,在该题中,比如我想求amount=11时的最少硬币数,假设有数值为1的硬币,其实就是在求amount=10的最少硬币数+1。由于硬币的数量是没有限制的,子问题之间没有相互制约,所以是相互独立的。

我们既然知道该题是动态规划的题了,那么就得认真思考一下它的状态转移方程了。

状态转移方程三部曲

先确定状态

状态即变量,在该题中唯一的状态是目标金额amount

确定dp函数定义

函数dp(n)表示,当前的目标金额n,至少需要dp(n)个硬币凑出该金额

确定选择并择优

对于每个状态,可以做出什么选择改变当前状态。在该题中,无论当前目标金额是多少,选择就是从coins中选一个硬币,然后将目标金额减去选择的硬币值。

好的,现在我们可以得到状态转移方程了

消除重叠子问题

我们使用自底向上的dp table来消除重叠子问题,dp数组的定义与dp函数的定义类似

解题思路

初始化一个长度为amount+1,每个元素初始值为amount+1的dp数组,因为组成amount的最大硬币数为amount(即全都是1的硬币数组成),初始化为amount+1相当于正无穷,便于后续取最小值

将第0位元素赋值为1,便于后续最小值比对

遍历dp数组,每次相当于计算目标金额为i的最小硬币数

每次遍历都要从coins中选择硬币做为dp[i-coin](即最优子结构)进行最小值比对

在选择硬币前还要判断子结构是否存在,即i<coin是无效的

代码

var coinChange = function (coins, amount) {
    let dp = Array(amount + 1).fill(amount+1)
    dp[0]=0
    for (let i = 0; i < dp.length; i++) {
        for (let coin of coins) {
            if (i < coin) {
                continue
            }
            dp[i] = Math.min(dp[i - coin]+1, dp[i])
        }
    }
    return dp[amount] == amount+1 ? -1 : dp[amount]
};