# 0322. 零钱兑换 ## 题目地址(322. 零钱兑换) <https://leetcode-cn.com/problems/coin-change/> ## 题目描述 ``` <pre class="calibre18">``` 给定不同面额的硬币 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 示例 4: 输入:coins = [1], amount = 1 输出:1 示例 5: 输入:coins = [1], amount = 2 输出:2 提示: 1 <= coins.length <= 12 1 <= coins[i] <= 231 - 1 0 <= amount <= 104 ``` ``` ## 前置知识 - 贪心算法 - [动态规划](https://github.com/azl397985856/leetcode/blob/master/thinkings/dynamic-programming.md) ## 公司 - 腾讯 - 百度 - 字节 - 阿里巴巴(盒马生鲜) ## 岗位信息 - 阿里巴巴(盒马生鲜):前端技术二面 ## 思路 ## 思路 假如我们把 coin 逆序排列,然后逐个取,取到刚好不大于 amout,依次类推。 ``` <pre class="calibre18">``` eg: 对于 [1,2,5] 组成 11 块 - 排序[5,2,1] - 取第一个5, 更新amout 为 11 - 5 = 6 (1⃣️) 6 > 5 继续更新 为 6 - 5 = 1 (2⃣️) 1 < 5 退出 - 取第二个2 1 < 2 退出 - 取最后一个元素,也就是1 1 === 1 更新为 1 - 1 = 0 (3⃣️) - amout 为 0 退出 因此结果是 3 ``` ``` 熟悉贪心算法的同学应该已经注意到了,这就是贪心算法,贪心算法更 amount 尽快地变得更小。 `经验表明,贪心策略是正确的`。 注意,我说的是经验表明, 贪心算法也有可能出错。 就拿这道题目来说, 他也是不正确的! 比如 `coins = [1, 5, 11] amout = 15`, 因此这种做法有时候不靠谱,我们还是采用靠谱的做法. 如果我们暴力求解,对于所有的组合都计算一遍,然后比较, 那么这样的复杂度是 2 的 n 次方(这个可以通过数学公式证明,这里不想啰嗦了), 这个是不可以接受的。那么我们是否可以动态规划解决呢?答案是可以,原因就是可以划分为子问题,子问题可以推导出原问题 对于动态规划我们可以先画一个二维表,然后观察,其是否可以用一维表代替。 关于动态规划为什么要画表,我已经在[这篇文章](dynamic-programming.html)解释了 比较容易想到的是二维数组: ``` <pre class="calibre18">``` <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">coinChange</span><span class="hljs-params">(self, coins: List[int], amount: int)</span> -> int:</span> <span class="hljs-keyword">if</span> amount < <span class="hljs-params">0</span>: <span class="hljs-keyword">return</span> - <span class="hljs-params">1</span> dp = [[amount + <span class="hljs-params">1</span> <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(len(coins) + <span class="hljs-params">1</span>)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(amount + <span class="hljs-params">1</span>)] <span class="hljs-title"># 初始化第一行为0,其他为最大值(也就是amount + 1)</span> <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(len(coins) + <span class="hljs-params">1</span>): dp[<span class="hljs-params">0</span>][j] = <span class="hljs-params">0</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-params">1</span>, amount + <span class="hljs-params">1</span>): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(<span class="hljs-params">1</span>, len(coins) + <span class="hljs-params">1</span>): <span class="hljs-keyword">if</span> i - coins[j - <span class="hljs-params">1</span>] >= <span class="hljs-params">0</span>: dp[i][j] = min( dp[i][j - <span class="hljs-params">1</span>], dp[i - coins[j - <span class="hljs-params">1</span>]][j] + <span class="hljs-params">1</span>) <span class="hljs-keyword">else</span>: dp[i][j] = dp[i][j - <span class="hljs-params">1</span>] <span class="hljs-keyword">return</span> <span class="hljs-params">-1</span> <span class="hljs-keyword">if</span> dp[<span class="hljs-params">-1</span>][<span class="hljs-params">-1</span>] == amount + <span class="hljs-params">1</span> <span class="hljs-keyword">else</span> dp[<span class="hljs-params">-1</span>][<span class="hljs-params">-1</span>] ``` ``` **复杂度分析** - 时间复杂度:O(amonut∗len(coins))O(amonut \* len(coins))O(amonut∗len(coins)) - 空间复杂度:O(amount∗len(coins))O(amount \* len(coins))O(amount∗len(coins)) dp\[i\]\[j\] 依赖于`dp[i][j - 1]`和 `dp[i - coins[j - 1]][j] + 1)` 这是一个优化的信号,我们可以将其优化到一维,具体见下方。 ## 关键点解析 - 动态规划 - 子问题 用 dp\[i\] 来表示组成 i 块钱,需要最少的硬币数,那么 1. 第 j 个硬币我可以选择不拿 这个时候, 硬币数 = dp\[i\] 2. 第 j 个硬币我可以选择拿 这个时候, 硬币数 = dp\[i - coins\[j\]\] + 1 3. 和背包问题不同, 硬币是可以拿任意个 4. 对于每一个 dp\[i\] 我们都选择遍历一遍 coin, 不断更新 dp\[i\] ## 代码 - 语言支持:JS,C++,Python3 JavaScript Code: ``` <pre class="calibre18">``` <span class="hljs-keyword">var</span> coinChange = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">coins, amount</span>) </span>{ <span class="hljs-keyword">if</span> (amount === <span class="hljs-params">0</span>) { <span class="hljs-keyword">return</span> <span class="hljs-params">0</span>; } <span class="hljs-keyword">const</span> dp = <span class="hljs-params">Array</span>(amount + <span class="hljs-params">1</span>).fill(<span class="hljs-params">Number</span>.MAX_VALUE); dp[<span class="hljs-params">0</span>] = <span class="hljs-params">0</span>; <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-params">1</span>; i < dp.length; i++) { <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> j = <span class="hljs-params">0</span>; j < coins.length; j++) { <span class="hljs-keyword">if</span> (i - coins[j] >= <span class="hljs-params">0</span>) { dp[i] = <span class="hljs-params">Math</span>.min(dp[i], dp[i - coins[j]] + <span class="hljs-params">1</span>); } } } <span class="hljs-keyword">return</span> dp[dp.length - <span class="hljs-params">1</span>] === <span class="hljs-params">Number</span>.MAX_VALUE ? <span class="hljs-params">-1</span> : dp[dp.length - <span class="hljs-params">1</span>]; }; ``` ``` C++ Code: > C++中采用 INT\_MAX,因此判断时需要加上`dp[a - coin] < INT_MAX`以防止溢出 ``` <pre class="calibre18">``` <span class="hljs-keyword">class</span> Solution { <span class="hljs-keyword">public</span>: <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">coinChange</span><span class="hljs-params">(<span class="hljs-params">vector</span><<span class="hljs-keyword">int</span>>& coins, <span class="hljs-keyword">int</span> amount)</span> </span>{ <span class="hljs-keyword">auto</span> dp = <span class="hljs-params">vector</span><<span class="hljs-keyword">int</span>>(amount + <span class="hljs-params">1</span>, INT_MAX); dp[<span class="hljs-params">0</span>] = <span class="hljs-params">0</span>; <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> a = <span class="hljs-params">1</span>; a <= amount; ++a) { <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> <span class="hljs-keyword">auto</span> & coin : coins) { <span class="hljs-keyword">if</span> (a >= coin && dp[a - coin] < INT_MAX) { dp[a] = min(dp[a], dp[a-coin] + <span class="hljs-params">1</span>); } } } <span class="hljs-keyword">return</span> dp[amount] == INT_MAX ? <span class="hljs-params">-1</span> : dp[amount]; } }; ``` ``` Python3 Code: ``` <pre class="calibre18">``` <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">coinChange</span><span class="hljs-params">(self, coins: List[int], amount: int)</span> -> int:</span> dp = [amount + <span class="hljs-params">1</span>] * (amount + <span class="hljs-params">1</span>) dp[<span class="hljs-params">0</span>] = <span class="hljs-params">0</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-params">1</span>, amount + <span class="hljs-params">1</span>): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(len(coins)): <span class="hljs-keyword">if</span> i >= coins[j]: dp[i] = min(dp[i], dp[i - coins[j]] + <span class="hljs-params">1</span>) <span class="hljs-keyword">return</span> <span class="hljs-params">-1</span> <span class="hljs-keyword">if</span> dp[<span class="hljs-params">-1</span>] == amount + <span class="hljs-params">1</span> <span class="hljs-keyword">else</span> dp[<span class="hljs-params">-1</span>] ``` ``` **复杂度分析** - 时间复杂度:O(amonut∗len(coins))O(amonut \* len(coins))O(amonut∗len(coins)) - 空间复杂度:O(amount)O(amount)O(amount) ## 扩展 这是一道很简单描述的题目, 因此很多时候会被用到大公司的电面中。 相似问题: [518.coin-change-2](https://github.com/azl397985856/leetcode/blob/master/problems/518.coin-change-2.md)