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

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

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

您可能感兴趣的与本文相关的镜像

Python3.11

Python3.11

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

<think>好的,我现在需要帮用户了解如何用Python改进找零兑换问题递归解法。首先,我得回忆一下找零问题的基本递归方法,然后思考如何优化它。 用户提到的递归解法改进,应该是指记忆化(Memoization)或者动态规划(DP)吧?因为普通的递归会有很多重复计算,效率很低。比如,当硬币面额是[1,2,5],要兑换10元的话,递归可能会多次计算兑换5元的情况,导致时间复杂度过高。 所以,我应该先解释普通递归问题,再引入记忆化的方法。比如,可以维护一个字典或者数组,保存已经计算过的金额所需的最少硬币数,避免重复计算。或者使用动态规划自底向上填的方式,这可能更高效。 接下来,我需要考虑如何将这个过程用Python代码实现。可能需要先写一个基本的递归版本,然后展示如何添加记忆化。比如,使用装饰器lru_cache,或者手动维护一个缓存字典。 但用户可能更希望看到手动实现的记忆化,这样更清楚原理。比如,在递归函数里,先检查当前金额是否已经计算过,如果有就直接返回结果,否则进行计算,并在返回前保存结果。 另外,需要注意硬币面额的遍历方式,确保不会漏掉可能的组合。比如,对于每个硬币,如果面额小于等于当前金额,就递归计算减去该面额后的金额所需硬币数,然后取最小值。 可能还需要处理无法兑换的情况,比如硬币面额无法组合成目标金额时,返回-1或者某种错误提示。但通常题目可能假设硬币包含1元,所以总能兑换,但代码里还是需要考虑这种情况。 然后,我需要验证这个改进后的方法是否真的有效。比如,比较优化前后的时间复杂度,普通递归是指数级的,而记忆化后是O(amount * coins),因为每个金额只计算一次。 另外,可能还需要提醒用户注意Python递归深度限制,如果金额很大,可能会导致栈溢出。这时候动态规划的迭代方法可能更合适,但用户的问题明确要求递归解法的改进,所以重点还是记忆化。 最后,总结步骤:普通递归问题,引入记忆化,代码实现,时间复杂度的对比,可能的使用示例和注意事项。这样用户就能清楚如何改进递归解法了。</think>要改进找零兑换问题递归解法,可以通过**记忆化(Memoization)**优化重复计算问题。以下是逐步实现思路: --- ### 一、问题定义 **找零问题**:给定硬币面额列 `coins` 和目标金额 `amount`,计算凑出该金额的**最少硬币数量**。若无法兑换则返回 `-1`。 --- ### 二、普通递归解法问题 普通递归重复计算相同子问题,例如: ```python def coin_change(coins, amount): if amount == 0: return 0 min_coins = float('inf') for coin in coins: if amount >= coin: res = coin_change(coins, amount - coin) if res != -1: min_coins = min(min_coins, res + 1) return min_coins if min_coins != float('inf') else -1 ``` - **时间复杂度**:$$O(S^n)$$(`S` 是金额,`n` 是硬币种类数),指数级效率极低。 --- ### 三、改进方法:记忆化递归 #### 核心思路 - 使用字典 `memo` 缓存已计算的金额结果,避免重复递归。 #### Python实现 ```python def coin_change_memo(coins, amount): memo = {} # 缓存字典:金额 -> 最少硬币数 def helper(remaining): if remaining in memo: return memo[remaining] # 直接返回已缓存的结果 if remaining == 0: return 0 min_coins = float('inf') for coin in coins: if remaining >= coin: res = helper(remaining - coin) if res != -1: min_coins = min(min_coins, res + 1) memo[remaining] = min_coins if min_coins != float('inf') else -1 return memo[remaining] return helper(amount) ``` #### 时间复杂度 - 优化至 $$O(S \cdot n)$$,每个金额只需计算一次。 --- ### 四、测试案例 ```python print(coin_change_memo([1, 2, 5], 11)) # 输出 3(5+5+1) print(coin_change_memo([2], 3)) # 输出 -1(无法兑换) ``` --- ### 五、进一步优化方向 1. **剪枝策略**:对硬币列排序后提前终止无效循环。 2. **动态规划**:改用自底向上的迭代方法(非递归,但效率更高)。 --- ### 六、总结 通过添加记忆化缓存,递归解法的时间复杂度从指数级降低到多项式级。此方法在代码结构清晰的同时显著提升性能,适合理解算法优化原理。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值