在我们使用递归算法时,可能会出现规模庞大的重复计算,用一个中间表记录每个计算过的最优解法,就可以避免大量的重复计算。中间结果记录可以很好解决找零兑换问题。实际上,这种方法还不能称为动态规划,而是叫做“memoization(记忆化/函数值缓存)”的技术提高了递归解法的性能。
1. 找零兑换的动态规划解法
动态规划算法采用了一种更有条理的方式来得到问题的解。
找零兑换的动态规划算法从最简单的“1分钱找零”的最优解开始,逐步递加上去,直到我们需要的找零钱数。
在找零递加的过程中,设法保持每一分钱的递加都是最优解,一直加到求解找零钱数,自然得到最优解。
递加的过程能保持最优解的关键是,其依赖于更少钱数最优解的简单计算,而更少钱数的最优解已经得到了。
问题的最优解包含了更小规模子问题的最优解,这是一个最优化问题能够用动态规划策略解决的必要条件。
originalamount找零兑换问题具体来说就是:

numCoins 就是当前需要找零的硬币个数。
1 5 10 25就是硬币的币值。
originalamount就是要找零的钱数。
例如用动态规划解决11分钱的兑换问题。
- 从1分钱兑换开始,逐步建立一个兑换表。

兑换5分钱的时候,最优解就是一个5分的硬币。
- 计算11分钱的兑换法,我们做如下几步:
- 首先减去1分硬币,剩下10分钱查表最优解是1,共需要2个硬币
- 然后减去5分硬币,剩下6分钱查表最优解是2,共需要3个硬币
- 最后减去10分硬币,剩下1分钱查表最优解是1,共需要2个硬币
25分就不减了,因为明显 11 < 24,不可能用25分来找零。

- 通过上述最小值得到最优解:2个硬币
解决思路如下:

need_change = 65
init_min_coins = [0] * (need_change + 1) # 建一个空表
real_value_list = [1, 5, 10, 25]
def dynamic_programming(change, min_coins, value_list):
for current_change in range(1, change + 1):
min_coins[current_change] = current_change # 最坏的情况是只用一分钱兑换
for value in value_list: # 遍历每个币值
if current_change >= value:
if 1 + min_coins[current_change - value] < min_coins[current_change]:
# 若使用当前币值时,需要的最小硬币个数最小,就更新最小硬币数表中的个数
min_coins[current_change] = 1 + min_coins[current_change - value]
return min_coins[change] # 直接查表,最后一个就是我们想要的结果
print(dynamic_programming(need_change, init_min_coins, real_value_list))
我们注意到动态规划算法的dynamic_programming并不是递归函数。虽然这个问题是从递归算法开始解决,但最终我们得到一个更有条理的高效非递归算法。
动态规划中最主要的思想是:
- 从最简单情况开始到达所需找零的循环
- 其每一步都依靠以前的最优解来得到本步骤的最优解,直到得到答案
2. 找零兑换:动态规划算法扩展
前面的算法已经得到了最少硬币的数量,但没有返回硬币如何组合。
扩展算法的思路很简单,只需要在生成最
优解列表同时跟踪记录所选择的那个硬币币值即可。
在得到最后的解后,减去选择的硬币币值,回溯到表格之前的部分找零,就能逐步得到每一步所选择的硬币币值。
need_change = 63
init_min_coins = [0] * (need_change + 1) # 建一个空表
init_coins_used = [0] * (need_change + 1) # 建一个空表用于保存每一步新增的硬币币值
real_value_list = [1, 5, 10, 21, 25]
def dynamic_programming(change, min_coins, value_list, coins_used):
for current_change in range(1, change + 1):
min_coins[current_change] = current_change # 最坏的情况是只用一分钱兑换
for value in value_list: # 遍历每个币值
if current_change >= value:
if 1 + min_coins[current_change - value] <= min_coins[current_change]:
# 若使用当前币值时,需要的最小硬币个数最小,就更新最小硬币数表中的个数
min_coins[current_change] = 1 + min_coins[current_change - value]
coins_used[current_change] = value # 当前使用的币值存入到表中
return min_coins[change], coins_used # 直接查表,最后一个就是我们想要的结果
def combination_values(change, coins_used):
print(coins_used[change])
while change > 1:
# 每次显示并减去当前新增的硬币,就是回溯了使用的所有币值,即显示了每次新增的硬币的币值
if coins_used[change - coins_used[change]] > 0:
print(coins_used[change - coins_used[change]])
change = change - coins_used[change]
min_coins, coins_used = dynamic_programming(need_change, init_min_coins, real_value_list, init_coins_used)
print(min_coins)
print(coins_used)
combination_values(need_change, coins_used)
3
[0, 1, 1, 1, 1, 5, 5, 5, 5, 5, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 25, 21, 21, 21, 25, 25, 25, 25, 25, 25, 25, 21, 21]
21
21
21
参考
本文的知识来源于B站视频 【慕课+课堂实录】数据结构与算法Python版-北京大学-陈斌-字幕校对-【完结!】,是对陈斌老师课程的复习总结
代码是我自己写的,功能比较简单