题目

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

image.png

思路

https://leetcode-cn.com/problems/coin-change/solution/dong-tai-gui-hua-tao-lu-xiang-jie-by-wei-lai-bu-ke/
贪心算法:

  1. 想要总硬币数最少,肯定优先用大面值硬币,所以 coins 按从大到小排序;
  2. 优先用大硬币凑

伪代码

  1. k = amount // coin
  2. amount = amount - k * coin
  3. count += k

但是这样做,会因为丢到了导致最后无法凑出总额。
比如:coins = [5, 3, 2,], amount = 9。
按照上述伪代码,就无法凑出总额。正确的硬币数该是5, 2, 2。

image.png
这是一个动态规划问题。动态规划问题,思考如何列出正确的状态转移方程?

  1. 确定base case。目标金额为0时,算法返回0,因为不需要任何coin就能凑出目标金额;
  2. 确定状态,也就是原问题和子问题中会变化的变量。coin的数量是无限多的,面额也是给定的,因此唯一的状态就是目标金额amount;
  3. 确定选择,也就会导致状态产生变化的行为。目标金额为什么变化?因为选择coin,每选择一枚coin,就相当于减少了目标金额。所以说所有的coins,就是选择;
  4. 明确dp函数的定义。参数就是状态,函数计算出的结果就是题目要求-凑出目标金额所需的最少硬币数量。

伪代码框架

  1. def coinChange(coins, amount):
  2. # 定义:要凑出金额n,至少要dp(n)个硬币
  3. def dp(n):
  4. # 做选择,选择需要硬币最少的那个结果
  5. for coin in coins:
  6. res = min(res, 1 + dp(n - coin))
  7. return res
  8. # 题目要求的最终结果是dp(amount)
  9. return dp(amount)

上述伪代码,加入base case即可得到最终答案。

  1. def coinChange(coins: List[int], amount: int):
  2. def dp(n):
  3. # base case
  4. if n == 0: return 0
  5. if n < 0: return -1
  6. # 求最小值,所以初始化为正无穷
  7. res = float('INF')
  8. for coin in coins:
  9. subproblem = dp(n - coin)
  10. # 子问题无解,跳过
  11. if subproblem == -1: continue
  12. res = min(res, 1 + subproblem)
  13. return res if res != float('INF') else -1
  14. return dp(amount)

image.png
image.png
通过备忘录消除子问题

  1. def coinChange(coins: List[int], amount: int):
  2. # 备忘录
  3. memo = dict()
  4. def dp(n):
  5. # 查备忘录,避免重复计算
  6. if n in memo: return memo[n]
  7. # base case
  8. if n == 0: return 0
  9. if n < 0: return -1
  10. res = float('INF')
  11. for coin in coins:
  12. subproblem = dp(n - coin)
  13. if subproblem == -1: continue
  14. res = min(res, 1 + subproblem)
  15. # 记入备忘录
  16. memo[n] = res if res != float('INF') else -1
  17. return memo[n]
  18. return dp(amount)
  1. def coinChange(coins: List[int], amount: int):
  2. # 备忘录
  3. memo = dict()
  4. def dp(n):
  5. # 查备忘录,避免重复计算
  6. if n in memo: return memo[n]
  7. # base case
  8. if n == 0: return 0
  9. if n < 0: return -1
  10. res = float('INF')
  11. for coin in coins:
  12. subproblem = dp(n - coin)
  13. if subproblem == -1: continue
  14. res = min(res, 1 + subproblem)
  15. # 记入备忘录
  16. memo[n] = res if res != float('INF') else -1
  17. return memo[n]
  18. return dp(amount)

零钱兑换 II

image.png

  1. class Solution:
  2. def change(self, amount: int, coins: List[int]) -> int:
  3. dp = [0] * (amount + 1)
  4. dp[0] = 1
  5. for coin in coins:
  6. for x in range(coin, amount + 1):
  7. dp[x] += dp[x - coin]
  8. return dp[amount]