今天大侠要讨论的题目是:【零钱兑换问题】
这是一道经典的动态规划问题. 此题目中,你将会看到动态规划全局最优解的体现。以及常见动态规划解题思路总结。
- 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。
如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11 输出: 3 解释: 11 = 5 + 5 + 1
来源:力扣
大侠借此题和大家说下动态规划 相比贪心算法的优势:
以上问题我们也可以用贪心算法(每次选择尽可能大的数值)来解释:
但是如果零钱集合是[1,5,7] 要配出的金额为10
则贪心算法会选择7和1,共需要4枚硬币
7
x
1
+
1
x
3
=
10
7x1+1x3 =10
7x1+1x3=10
但是最优解是选择5, 共需要2枚硬币
5
∗
2
=
10
5*2=10
5∗2=10
由此可见,使用动态规划考虑全局问题是非常有必要的。
解决动态规划问题 主要分为以下三步:
- 目标(objection)
- 定义状态(define status)
- 转化方程(transform equation)
下面和大侠一起分析下这个问题吧!
目标 :凑成总金额所需的最少的硬币个数
定义状态:M(j)表示凑成j金额 所需的最少的硬币个数
转化方程:
M
[
j
+
1
]
=
{
m
i
n
(
M
[
j
−
c
o
i
n
s
[
i
]
]
+
1
)
j
>
=
c
o
i
n
s
[
i
]
a
c
c
o
u
n
t
+
1
e
l
s
e
M[j+1] = \begin{cases} min( ~M[j-coins[i]]~+1~) &j>=coins[i] \\ account+1 & else \end{cases}
M[j+1]={min( M[j−coins[i]] +1 )account+1j>=coins[i]else
以上公式的意思是,如果我当前要配出10 给出的硬币是1,5,7
由于 10 大于1,5,7 ,所以我有三种选择:
M
(
10
)
=
m
i
n
(
M
[
10
−
1
]
+
1
,
M
[
10
−
5
]
+
1
,
M
[
10
−
7
]
+
1
)
M(10)=min(M[10-1]+1,~M[10-5]+1,~M[10-7]+1)
M(10)=min(M[10−1]+1, M[10−5]+1, M[10−7]+1)
下面讲下算法实现:
首先对于传入 参数的进行判断(boundary case)
- 如果account<0 不合法
- 如果coins为空 ,不合法
- 如果coins中的最小值都大于account ,无法配出
接下来循环计算记忆数组
- 初始化数组,并将m[0]=0 表示当account=0 时,不需要硬币
- 根据公式 循环计算每一个m的值
tips:
由于无法确定coins中的元素是否都满足条件 coins[i]<j
所有要通过for循环来逐个比较,并通过min_c 记录最少硬币数,等遍历结束coins ,将min_c 的数据写入m列表。account+1 作为无法配出的标志位。
# # 零钱兑换.py
# # description : 使用不同面值的硬币配出给定的面值.求最少硬币个数.
def solution(account,coins):
# Definition: coins=[1,5,7] account = 10
#boundary case
if account <=0 :
return 0
if len(coins)==0:
return -1
if min(coins)>account:
return -1
#init memory sequence
m=[-1 for _ in range(account+1)]
m[0]=0
for j in range(1,account+1):
min_c=account+1#定义无法配出的标志
for c in coins:
if c<=j:
min_c =m[j-c] if m[j-c]<min_c else min_c
m[j]=min_c +1 if min_c <account+1 else account+1
if m[-1]==account+1:#表示无法配出
return -1
else:
return m[-1]#M[-1]表示配出account所需要的最少硬币数。
print(solution(10,[1,5,7]))
有什么问题,欢迎留言讨论。如果有幸帮到你,欢迎点赞哦,摸摸哒!