每日一题 零钱兑换 II
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
解题思路
是昨天写的零钱兑换的后续,但是这次需要的是计算并返回所有可行的组合数。还是使用动态规划的思路
使用一个dp数组储存金额为i,的组合数,没有组合能凑成0,因此dp[0]=0。
我们遍历coins数组中不同面额的硬币coin,若金额“i”比硬币的任何一个面额都要小,那它必然不能被组合出来,即dp[i]=0,所以我们只需要关注的金额。
若金额“i-coin"是一个可被组合出来的金额数,就说明金额“i"的也可被组合(两者之间差一个coin的面额),因此可以使用dp[i-coin]的值更新dp[i]:
但当i==coin时,也至少有一个组合为一枚面额为coin的硬币,为方便计算,我们将dp[0]初始化为1。
如coins[2, 3],amount=5,流程如下:
i = 2
dp[2] += dp[0] # dp[2] = 1
dp[3] += dp[1] # dp[1]还未被计算,初始化为0,dp[3] = 0
dp[4] += dp[2] # dp[4] = 1
dp[5] += dp[3] # dp[5] = 0
i = 3
dp[3] += dp[0] # dp[3] = 1
dp[4] += dp[1] # dp[4] = 1
dp[5] += dp[2] # dp[5] = 0
通过上述计算过程我们便得到了amount为1到5的所有组合数,并储存在了dp数组的对应位置。
第二种实现方法是使用记忆化搜索。
我们可以定义了一个内部递归函数dfs来计算硬币组合数,使用备忘录memo来存储已经计算过的结果,避免重复计算。递归函数中,我们可以选择包含当前硬币和不包含当前硬币两种情况,然后将结果累加并返回。
代码实现
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i-coin]
return dp[amount]
def change(self, amount: int, coins: List[int]) -> int:
memo = {}
def memery(amount, index):
if amount == 0:
return 1
if amount < 0 or index == len(coins):
return 0
if (amount, index) in memo:
return memo[(amount, index)]
include_ = memery(amount-coins[index], index)
disinclude_ = memery(amount, index + 1)
memo[(amount, index)] = include_ + disinclude_
return memo[(amount, index)]
return memery(amount, 0)
复杂度分析
动态规划:时间O(n*amount),其中n是硬币数组的长度,空间O(amount)
记忆化搜索:时间O(n*amount), 空间O(n*amount)