当前位置: 首页 > news >正文

动态规划:硬币兑换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层。一个神来之笔的思考是,我们可以使用递归来解这种嵌套的问题

0枚5元
1枚5元
2枚5元
0枚2元
i枚2元
6枚2元
0枚2元
i枚2元
3枚2元
0枚2元
1枚2元
amount=12
coins=[1,2,5]
amount=12
coins=[1,2]
amount=7
coins=[1,2]
amount=2
coins=[1,2]
amount=12
coins=[1]
amount=12-i*2
coins=[1]
amount=0
coins=[1]
amount=7
coins=[1]
amount=7-i*2
coins=[1]
amount=1
coins=[1]
amount=2
coins=[1]
amount=0
coins=[1]

递归的每一层表示选择哪个硬币,例如第一层选择的是5元硬币,枚举amount // 5 + 1个5元硬币,剩下的金额使用[1,2]的面额来兑换,第二层选择了2元硬币,然后接着枚举remain // 2 + 1个2元硬币,以此类推

i=0
i=1
i
i=k
i=0
i=1
i
i=k
i=0
i=1
i
i=k
dfs(amount, coins)
dfs(amount-0*coins[0],coins)
dfs(amount-1*coins[0],coins)
...
dfs(amount-i*coins[0],coins)
...
dfs(amount-k*coins[0],coins)
dfs(amount-0*coins[1],coins)
dfs(amount-1*coins[1],coins)
...
dfs(amount-i*coins[1],coins)
...
dfs(amount-k*coins[1],coins)
dfs(amount-0*coins[2],coins)
dfs(amount-1*coins[2],coins)
...
dfs(amount-i*coins[2],coins)
...
dfs(amount-k*coins[2],coins)

按照上面的递归写出代码,补充一个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个集合的方案数相加即是最终的方案数

0枚5元
1枚5元
2枚5元
0枚2元
i枚2元
6枚2元
0枚2元
i枚2元
3枚2元
0枚2元
1枚2元
amount=12
coins=[1,2,5]
amount=12
coins=[1,2]
amount=7
coins=[1,2]
amount=2
coins=[1,2]
amount=12
coins=[1]
amount=12-i*2
coins=[1]
amount=0
coins=[1]
amount=7
coins=[1]
amount=7-i*2
coins=[1]
amount=1
coins=[1]
amount=2
coins=[1]
amount=0
coins=[1]

所以其实路径这个参数我们是不需要的,如果通过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])
http://www.dtcms.com/a/361815.html

相关文章:

  • 异常类分析
  • HTML应用指南:利用GET请求获取全国招商银行网点位置信息
  • 软件测试面试技巧-面试问题大全
  • 盟接之桥说制造:守正出奇:在能力圈内稳健前行,以需求导向赢得市场
  • 综合实验:DHCP、VLAN、NAT、BDF、策略路由等
  • 数据库主键选择策略分析
  • 【高级】系统架构师 | 2025年上半年综合真题
  • Linux系统结构(概要)
  • 实现一个线程池管理器
  • 数字后端tap cell:新老工艺tap cell区别
  • 人工智能视频画质增强和修复软件Topaz Video AI v7.1.1最新汉化,自带星光模型
  • 网络编程5-数据库、sqlite3数据库
  • 多级渐远纹理(Mipmap):原理、生成、采样与 OpenGL 实践
  • 2025 金融行业证书怎么选?从能力适配到职业方向的理性梳理
  • 7-ATSAM3X8-DAC输出
  • 网络与信息安全有哪些岗位:(13)安全服务工程师 / 顾问
  • 机器学习——损失函数
  • leetcode-python-1796字符串中第二大的数字
  • LeetCode82删除排序链表中的重复元素 II
  • wpf之样式
  • 嵌入式解谜日志之Linux操作系统—共享内存
  • Python备份实战专栏第5/6篇:Docker + Nginx 生产环境一键部署方案
  • 基于多种分词算法的词频统计的中文分词系统的设计与实现
  • 信创之-麒麟v10服务器安装tengine(已完成)
  • 推荐系统中Redis 数据存储:二进制序列化协议选型与优化
  • linux连接服务器sftp无法输入中文
  • 基于SpringBoot的教务管理系统(源码+文档)
  • C/C++ Linux系统编程:进程通讯完全指南,管道通讯、共享内存以及消息队列
  • 零基础从头教学Linux(Day 25)
  • vue3使用Eslint