Python数据结构19:找零兑换问题的递归解法,用表保存当前最优,避免重复计算的方法

本文探讨了贪心算法在货币找零问题上的局限性,并通过递归方法提出了解决方案。递归解法遵循递归三定律,但效率低下。为提高效率,通过使用已计算的最优解表格避免了重复计算,显著减少了计算次数,实现了高效的找零算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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版-北京大学-陈斌-字幕校对-【完结!】,是对陈斌老师课程的复习总结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值