1.找零问题的贪心算法失效的情况
对于一个货币找零问题,当货币的币值是按照贪心算法设计时,就可以用贪心策略解决。但是如果货币的币值比较特殊时,贪心算法可能失效。
例如:假设你为一家自动售货机厂家编程序,自动售货机要每次找给顾客最少数量硬币;假设某次顾客投进$1(1美元)纸币,买了ȼ37(37美分)的东西,要找ȼ63,那么最少数量就是:2个quarter(ȼ25)、1个dime(ȼ10)和3个penny(ȼ1),一共6个硬币。但是如果有一种21美分的硬币时,只需要63 / 21 = 3个硬币就可以解决这个找零问题。此时贪心算法就失效了。
2.递归解法
我们需要一种肯定能解决找零问题的算法。用递归的思想来解决这个问题。
回顾一下递归三定律:
1.递归算法必须有一个基本结束条件。(最小规模的问题可以直接解决)
2.递归算法必须能改变状态,向基本结束条件演进。(减小问题的规模)
3.递归算法可以调用自身。(解决规模更小的相同问题)
套用到这个找零的问题(找零的币值是1美分,5美分,10美分,25美分),代码和思路如下:
def change_problem(value_list, change): # 找到需要兑换的最小的硬币个数
min_coins = change # 初始情况时,最坏的情况时是最少需要change个一美分来兑换
if change in value_list: # 基本结束条件:如果能找到一种币值刚好就是要兑换的钱数,一个硬币就行了
return 1
else:
for i in value_list: # 此时,没有一种特定的币值可以直接找零,依次遍历每一种币值,哪一种币值使用后兑换的硬币个数最少就返回哪一种硬币个数
if change > i:
current_coins = 1 + change_problem(value_list, change - i) # 要注意每次都用了一个一枚硬币,所以要加上一个
# change - i就是减少问题的规模,当使用当前的这个币值找零时,后续还需要对change - i这么多钱兑换,找到兑换(chang - i)
# 需要的最少硬币数
if current_coins < min_coins: # 在使用这个币值时,需要兑换的硬币个数小于之前的
min_coins = current_coins
return min_coins
print(change_problem([1, 5, 10, 25], 63))
print(change_problem([1, 5, 10, 21, 25], 63))
3. 递归算法的优化
上面这个算法虽然能够解决问题,但是效率太低了,在我的笔记本上需要花费17秒的时间。
print(time.process_time())
print(change_problem([1, 5, 10, 25], 63))
print(time.process_time())
0.046875
6
17.484375
对于上述的递归算法,兑换63分的硬币,需要进行67716925次递归调用。
以26分兑换硬币为例,看看递归调用过程:

这个递归算法,每次都是分别尝试所有币值是否可能是需要的最少硬币数,因此在面值大时,要计算四次。导致重复计算有很多,例如找零15分的,出现了3次。而最终解决这个问题还需要52次递归调用。很明显,这个算法致命缺点是重复计算。
对这个递归解法进行改进的关键就在于消除重复计算。
我们可以用一个表将计算过的中间结果保存起来,在计算之前查表看看是否已经计算过。
这个算法的中间结果就是部分找零的最优解,在递归调用过程中已经得到的最优解被记录下来。

比如在第二行标黄色的部分就已经计算过了对16的找零的最小策略,直接将其记录下来,在下面一行标蓝色的部分,就不需要再计算一次了。
在递归调用之前,先查找表中是否已有部分找零的最优解。
如果有,直接返回最优解而不进行递归调用,如果没有,才进行递归调用。
from time import process_time
need_change = 63
value_list = [25, 10, 5, 1]
init_known_list = (need_change + 1) * [0] # 要加一是因为数组的下标是从0开始的,也就是说known_list[63]其实是数组的第64个位置
def change_problem(vl, change, known_list): # 大规模问题是,找零可以兑换的最少的硬币个数。
min_num = change
if change in vl:
return 1
# 基本结束条件
elif known_list[change] != 0: # 如果在表里可以直接找到最优解,直接返回就得了
return known_list[change]
else: # 在已知的最优解当中找不到当前的最优,朝更小的规模演进,调用自身
for i in vl:
if change > i:
current_num = 1 + change_problem(vl, change - i, known_list)
if current_num < min_num:
min_num = current_num
known_list[change] = min_num # 将当前计算到的最少兑换硬币数,记录到最优解的表中。
return min_num
print(process_time())
print(change_problem(value_list, need_change, init_known_list))
print(process_time())
结果如下:
0.0625
6
0.0625
很明显,速度快了特别多。
参考
本文的知识来源于B站视频 【慕课+课堂实录】数据结构与算法Python版-北京大学-陈斌-字幕校对-【完结!】,是对陈斌老师课程的复习总结