动态规划:硬币兑换II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
示例
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
暴力求解
对于我这样的初学者而言,所有的算法都是从暴力求解开始优化的,因此第一步就是思考如何通过暴力算法求出问题解。
一个简单的思路就是,枚举每一枚硬币的数量,然后计算出是否符合题目要求,即凑成总金额。
def coinChange(amount=5, coins=[1,2,5]):res = []# 枚举所有1元硬币的数量for i in range(amount // 1+1):# 枚举所有2元硬币的数量for j in range(amount // 2+1):# 枚举所有5元硬币的数量for k in range(amount // 5+1):# 判断是否可以组成金额if i * 1 + j * 2 + k * 5 == amount:res.append((i,j,k))return len(res)
暴力求解的思路非常简单,唯一比较难以处理的地方就是数组的个数是变化的,如果coins有100种金额,for循环的深度就要有100层。一个神来之笔的思考是,我们可以使用递归来解这种嵌套的问题
递归的每一层表示选择哪个硬币,例如第一层选择的是5元硬币,枚举amount // 5 + 1
个5元硬币,剩下的金额使用[1,2]
的面额来兑换,第二层选择了2元硬币,然后接着枚举remain // 2 + 1
个2元硬币,以此类推
按照上面的递归写出代码,补充一个depth表示当前递归到了哪一层,这一层表示选择哪个面额的硬币。
def coin_change_combinations(amount, coins):res = []def dfs(amount, coins, depth, path):# 如果剩余金额为0,说明正好兑换完成if remaining == 0:# 将结果添加进来res.append(path[:])return# 如果没有可用硬币或剩余金额为负,直接返回if not coins or amount < 0:return# 尝试使用当前硬币的不同数量max_usage = amount // coins[depth]for i in range(max_usage + 1):# 计算剩余金额remaining = amount - i * coins[0]# 递归处理剩余金额和剩余硬币,并将之前的硬币的取值传递下dfs(remaining, coins, depth + 1, path + [(coins[0], i)])# 开始深度优先搜索dfs(amount, coins, [])return res
每次向下一层传递路径的时候,其实是拷贝了一份新的数据传递下去,我们可以简单优化一下
def coin_change_combinations(amount, coins):res = []def dfs(amount, coins, path, depth):if amount == 0:res.append(path[:])returnif len(coins) == depth or amount < 0:returnmax_usage = amount // coins[depth]for i in range(max_usage + 1):# 计算剩余金额remaining = amount - i * coins[depth]# 将当前硬币的使用数量添加到路径中path.append((coins[depth], i))# 递归处理剩余金额和剩余硬币dfs(remaining, coins, path, depth+1)# 回溯,移除最后添加的数量path.pop()dfs(amount, coins, [], 0)return resres = coin_change_combinations(12,[1,2,5])
for p in res:print(p)
集合划分
暴力方法是求出所有的枚举组合,但是我们要求解的是有多少种组合的数量,也就符合条件的方案数。我们按照5元面的数量将划分成3个集合,无需枚举2枚以后5元硬币,因为3枚5元硬币的金额已经大于12元了,不可能存在满足条件的方案。
- 集合1,使用0枚5元硬币可以兑换成功的方案数
- 集合2,使用1枚5元硬币可以兑换成功的方案数
- 集合3,使用2枚5元硬币可以兑换成功的方案数
对于每个集合我们可以再次细分方案
- 对于集合1而言,使用1和2来兑换出12-5*0=12元金额
- 对于集合2而言,使用1和2来兑换出12-5*1=7元金额
- 对于集合3而言,使用1和2来兑换出12-5*2=2元金额
集合1的方案中,必然不会出现5元硬币,例如[2,2,2,2,2,2],[2,2,2,2,2,1,1]
等等
集合2的方案中,必然只会使用1枚5元硬币,例如[5,2,2,2,1],[5,2,2,1,1,1],[5,2,1,1,1,1,1]
等等
集合3的方案中,必然只会使用2枚5元硬币,例如[5,5,2],[5,5,1,1]
通过这种划分,我们其实可以不重不漏的将所有的方案进行分类,每个方案必属于某个集合,这3个集合的方案数相加即是最终的方案数
所以其实路径这个参数我们是不需要的,如果通过dfs(remaining, coins)
有解,那么我们就加1即可
def coin_change_combinations(amount, coins):res = []def dfs(amount, coins, path, depth):if amount == 0:return 1if len(coins) == depth or amount < 0:return 1max_usage = amount // coins[depth]for i in range(max_usage + 1):# 计算剩余金额remaining = amount - i * coins[depth]# 递归处理剩余金额和剩余硬币dfs(remaining, coins, path, depth+1)dfs(amount, coins, [], 0)return resres = coin_change_combinations(12,[1,2,5])