如何优化零钱兑换记忆化DP解法的运行时性能?
问题场景
给定整数数组coins表示不同面额的硬币,整数amount表示目标总金额,需返回凑出该总金额所需的最少硬币数量;若不存在任何硬币组合可凑出目标金额则返回-1,题目约定每种硬币可无限次使用。
咨询问题
当前实现的硬币找零解法是否存在提速空间,如何优化运行效率?
当前解法采用记忆化回溯思路,定义dp[curr_amount]存储从当前金额curr_amount到达目标金额所需的最少额外硬币数量,代码实现如下:
def coinChange(self, coins: List[int], amount: int) -> int: if amount == 0: return 0 dp = {} def backtrack(curr_amount): if (curr_amount) in dp: return dp[curr_amount] if curr_amount == amount: return 0 if curr_amount > amount: return inf for coin in coins: if (curr_amount) not in dp: dp[curr_amount] = inf # 遍历所有可选硬币,取所需最少硬币数 dp[curr_amount] = min(dp[curr_amount], 1 + backtrack(curr_amount + coin)) return dp[curr_amount] res = backtrack(0) if res == inf: return -1 return res
优化方案
该解法的核心记忆化思路是正确的,但存在多处可优化的点,优化后运行效率可提升数倍:
- 替换记忆化存储结构:当前用字典做dp存储,哈希表的查询、写入存在额外哈希计算开销,由于所有待计算的金额范围固定为
0~amount,直接用长度为amount+1的定长数组存储状态,访问时直接按下标索引,读写速度远高于字典。 - 消除递归开销:当前自顶向下的递归写法存在函数调用栈开销,当amount值较大时还有触发Python递归深度限制的风险,可以改成自底向上的递推写法,从金额0开始逐步计算到目标金额,完全规避递归相关的额外消耗。
- 多维度剪枝减少无效计算:
- 预处理硬币数组,直接筛掉所有面额大于
amount的硬币,这类硬币不可能出现在合法组合里,遍历属于纯无效操作; - 将剩余硬币按面额从大到小排序,优先尝试大面额硬币,能更早得到一个较小的硬币数参考值,后续分支如果当前累计硬币数已经超过参考值,可以直接提前终止,砍掉大量无效分支;
- 遍历硬币时提前判断当前硬币面额是否超过剩余待凑金额,直接跳过不符合要求的硬币,避免进入无意义的递归/计算分支;
- 原代码中
if (curr_amount) not in dp的判断放在硬币遍历循环内,每次循环都会重复执行,完全可以提到循环外只判断一次,减少重复判断开销。
- 预处理硬币数组,直接筛掉所有面额大于
- 调整状态定义简化逻辑:把原定义「从curr_amount到amount需要的最少硬币」改成「凑出金额curr_amount需要的最少硬币」,递推逻辑更直观,也能减少边界判断的代码量。
优化后的参考实现如下:
def coinChange(self, coins: List[int], amount: int) -> int: # 边界情况直接返回 if amount == 0: return 0 # 预处理硬币:筛掉超大额硬币,降序排序 valid_coins = sorted([c for c in coins if c <= amount], reverse=True) # 用数组存dp,初始值设为amount+1(等价于不可达,因为凑amount最多用amount个1元硬币) dp = [amount + 1] * (amount + 1) dp[0] = 0 for i in range(1, amount + 1): for coin in valid_coins: if coin <= i: dp[i] = min(dp[i], dp[i - coin] + 1) return dp[amount] if dp[amount] != amount + 1 else -1
内容的提问来源于stack exchange,提问作者Michael Xia
相关产品推荐
相关产品推荐

