题目描述
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例:
coins = [1, 2, 5]
amount = 100
返回 20
coins = [1, 2, 5]
amount = 103
返回 22
解法一:动态规划(自底向上)
分析:
假设 coins = [coin1, coin2, coin3],amount = N
我们要计算的是,用coins里面的硬币来组成N,并且硬币数最少。
我们可以这么思考,假设有一个硬币池,里面有无数个coin1, coin2, coin3,我们不断地从里面挑硬币出来,直到总金额达到N。那被挑出来的这些硬币就是加起来等于N的一个组合,当然不同的挑法,肯定有不同的组合,我们要做的就是在这些组合里面找到最少的硬币个数就行了。
这里我们再逆向思考一下,在每个符合规定(相加总金额等于N)的组合里面,最后挑出来的硬币肯定是coin1,coin2,coin3其中之一。假设f(N)表示组成N的最少硬币数,那么就有如下关系:
f(N) = min(f(N-coin1), f(N-coin2), f(N-coin3)) + 1
这个公式在动态规划里面叫做状态转移方程,其实很好理解,我们计算f(N), 实际上可以转化为计算f(N-coin1)、f(N-coin2)、f(N-coin3),然后取它们的最小值,再加1(因为我减去了一个硬币)。
那自底向上的思想就是,我们要计算f(N),那我们依次计算出f(1)、f(2)、f(3)…f(N-coin1)、f(N-coin2)、f(N-coin3),这样我们就可以轻松的得到f(N)了。
代码实现:
def solution(coins: list, amount: int) -> int:
# 定义一个数组,用于存储组成0-amount每一个面额的最小硬币数,初始化为无限大
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins: # 遍历硬币面额
# 遍历每一个总金额,为了保证x - coin >= 0,我们从大于coin开始遍历
for x in range(coin, amount + 1):
# 以下代码是核心
dp[x] = min(dp[x], dp[x - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
解法二:递归
递归的解法,主要是要找到递进关系和结束递归的条件.
- 递进关系 f(N) = min(f(N-coin1), f(N-coin2), f(N-coin3)…) + 1
- 结束递归的条件,N <= 0
代码实现:
import functools
def solution(coins: list, amount: int) -> int:
@functools.lru_cache(amount) # 把函数调用结果记录到缓存,记录次数为amount
def dp(_amount):
if _amount < 0:
return -1
if _amount == 0:
return 0
mini = int(1e9)
for coin in coins:
# 进入递归,计算f(N-coin1)、 f(N-coin2)、f(N-coin3)...
res = dp(_amount - coin)
if 0 <= res < mini:
mini = res + 1
return mini if mini < int(1e9) else -1
if amount < 1:
return 0
return dp(amount)