阅读 176

LeetCode 刷题笔记 - 322. 零钱兑换

难度:

中等

描述:

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例:

1:
输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1
复制代码
2:
输入: coins = [2], amount = 3
输出: -1
复制代码

说明: 你可以认为每种硬币的数量是无限的。

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/co… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


语言:

swift

解析:

这是一道比较基础的关于动态规划的题。那我们就按着动态规划的标准解题步骤来一步一步解答。
我们需要拆分子问题,找到状态转移方程。
首先拆分子问题:

1. 如何用最少的 coins 凑够 0 元?
2. 如何用最少的 coins 凑够 1 元?
3. 如何用最少的 coins 凑够 2 元?
4. ...
5. 如何用最少的 coins 凑够 n 元?
复制代码

我们分别解决这些子问题,我们用d(i) = j来表示,凑够i元最少需要j个硬币:

1. 如何用最少的 coins 凑够 0 元?
复制代码

对于该情况,显然是需要0枚硬币就能凑够0元,故:

d(0) = 0
复制代码
2. 如何用最少的 coins 凑够 1 元?
复制代码

为了凑够1元,我们需要1枚1元的硬币,同时我们要和上一个问题想一想有没有什么关联?为了解决子问题2,就等于先解决了子问题1,在解决了子问题1的基础解决了子问题2。

d(1) = d(1 - 1) + 1 = d(0) + 1 = 0 + 1 = 1
// d(1) 代表为了凑够 1 元
// d(1 - 1) + 1 代表为了解决这个问题,我们需要解决凑够 0 元情况下的结果,再加上凑够 1 元需要的一块的这一种结果
复制代码
3. 如何用最少的 coins 凑够 2 元?
复制代码

为了求出最少凑够2元,我们同样可以得出:解决问题2,凑够1元的情况下,再凑够1元:

d(2) = d(2 - 1) + 1 = d(1) + 1 = 1 + 1 = 2
// d(2) 代表为了凑够 2 元
// d (2 - 1) + 1 代表为了解决这个问题,我们需要解决凑够 1 元的情况下,再加上凑够剩下的 1 元
复制代码

但是题目上,我们不仅有1元的硬币,还有2元的硬币,这个时候,我们仅需要一枚2元的硬币就可以凑够2元,也就是:凑够 2 元 = 凑够 0 元 + 凑够 2 元

d(2) = d(2 - 2) + 1 = d (0) + 1 = 0 + 1 = 1
// d(2) 代表为了凑够 2 元
// d(2 - 2) + 1 代表可以将凑够二元分解为,先凑够 0 元,再用 1 个 2 元的硬币来凑够 2 元
复制代码

其实在这个时候,d(2) = 1,可以将上面的两个方程简化一下为:
d(2) = min{d(2 - 1) + 1, d(2 - 2) + 1}
那么我们就可以抽象成以下状态转移方程:
d(n) = min{d(n - c) + 1}
意思为,为了凑够 n 元,我们需要找到不同的面值 c 下的最优解。
所以为了凑够 11 元,本题来说,我们需要如下状态方程:

d(11) = min{d(11 - 1) + 1, d(11 - 2) + 1, d(11 - 5) + 1}
      = min{d(10) + 1, d(9) + 1, d(6) + 1}
d(10) = ...
d(9) = ...
d(6) = ...
...
复制代码

编程的话,也就是需要将状态转移方程表达出来。我们需要一个dict来存储凑够不同价格时候的最优解。可以循环的条件为amount - coin >= 0,举例为如果硬币只有10元面值,而需要凑够5元,这时候5 - 10 < 0,不满足条件。

代码如下:

class Solution {
    func coinChange(_ coins: [Int], _ amount: Int) -> Int {
        if amount == 0 {
            return 0
        }
        var dict = [Int : Int]()
        dict[0] = 0
        for index in 1...amount {
            var minLength = -1
            for coinsIndex in 0...coins.count - 1 {
                let previousValue = index - coins[coinsIndex]
                if previousValue >= 0 {
                    if (dict[previousValue] != nil) && dict[previousValue]! != -1 {
                        if minLength == -1 {
                            minLength = dict[previousValue]! + 1
                        } else {
                            minLength = min(minLength, dict[previousValue]! + 1)
                        }
                    }
                }
            }
            dict[index] = minLength
        }
        return dict[amount]!
    }
}
复制代码

总结

动态规划很有趣,需要好好研究一下。