破解零钱兑换难题:从暴力尝试到动态规划的思维跃迁

88 阅读4分钟

一、当自动售货机遇见算法难题

想象你站在一台智能售货机前,投入11元纸币想买饮料,机器里有无数1元、2元、5元硬币。如何用最少的硬币完成找零?这道看似简单的日常生活问题,正是LeetCode经典题目「322. 零钱兑换」的现实映射。

1.1 问题本质剖析

  • 输入:硬币面额数组(如[1,2,5]),目标金额(如11)
  • 输出:凑成金额的最少硬币数(无法凑出返回-1)
  • 隐藏条件:每种硬币可以使用无限次

二、暴力递归的困境

简单的思路是暴力枚举所有组合:

function bruteForce(amount) {
    if(amount === 0) return 0;
    let min = Infinity;
    for(let coin of coins) {
        if(amount >= coin) {
            min = Math.min(min, 1 + bruteForce(amount - coin));
        }
    }
    return min;
}

但当amount=30时,计算次数呈指数爆炸:

计算f(30)需要计算f(29)、f(28)...  
每个子问题又产生新的递归调用  
时间复杂度:O(S^n)(S为金额,n为硬币种类)

三、动态规划的破局之道

3.1 状态定义的智慧

我们引入dp[i]表示凑出金额i所需的最少硬币数。这个定义就像给每个金额分配一个智能管家,记录最优解。

3.2 状态转移方程揭秘

对每个金额i,我们检查所有硬币:

dp[i] = min(dp[i - coin] + 1)  (对所有coin <= i

这个过程就像玩拼图游戏,用已有的最优解拼出更大的解。

3.3 代码逐行解析

const coinChange = (coins, amount) => {
    const dp = []; 
    dp[0] = 0; // 初始化:0元需要0个硬币
    
    for(let i=1; i<=amount; i++){
        dp[i] = Infinity; // 初始化为无法到达
        for(const coin of coins){
            if(coin <= i) {
                // 关键转移:用coin面额硬币后的最优解
                dp[i] = Math.min(dp[i], dp[i - coin] + 1); 
            }
        }
    }
    
    return dp[amount] === Infinity ? -1 : dp[amount];
};

四、算法执行全透视

示例分析:coinChange([1, 2, 5], 11)

4.1 初始化

  • f = [0, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞]
  1. 动态填充过程

    金额 i尝试硬币计算过程f[i]
    11min(∞, f[0]+1) = 11
    21,2min(∞, f[1]+1=2, f[0]+1=1)1
    31,2min(∞, f[2]+1=2, f[1]+1=2)2
    41,2min(∞, f[3]+1=3, f[2]+1=2)2
    51,2,5min(∞, f[4]+1=3, f[3]+1=3, f[0]+1=1)1
    ............
    111,2,5min(∞, f[10]+1=3, f[9]+1=3, f[6]+1=3)3
  2. 最终结果

    f[11] = 3(5+5+1)

4.2 关键点说明

  1. 状态定义

    f[i] 表示凑出金额 i 的最小硬币数

  2. 边界条件

    f[0] = 0:零金额需要零个硬币 f[other] = Infinity:初始化为不可达

  3. 时间复杂度

    O(amount × n),其中 n 是硬币种类数

五、复杂度优化的艺术

5.1 时间复杂度

  • 外层循环:O(amount)
  • 内层循环:O(n)
    总复杂度:O(amount * n)

当amount=1e4,n=100时,需要1e6次操作,完全在合理范围。

5.2 空间优化技巧

使用滚动数组可将空间复杂度降至O(max(coins)):

const dp = new Array(maxCoin + 1).fill(Infinity);

六、常见陷阱与突破

6.1 贪心算法的失效

当coins=[3,5]时,贪心选择5元硬币:

  • amount=8:5+3=8(2枚)
  • 贪心解法:5+?→ 无法凑出
    而动态规划能正确处理这种情况。

6.2 边界条件处理

  • 金额为0时需要返回0
  • 硬币数组为空时的特殊处理
  • 存在面额大于目标金额的硬币

七、从算法到架构的思考

在支付系统的零钱找零模块中,这种算法可以:

  1. 预计算常见金额的最优解
  2. 结合缓存机制加速响应
  3. 动态调整硬币库存状态
  4. 生成多种找零方案供选择

当你在自动售货机前等待找零时,背后可能正运行着类似的动态规划算法。理解这个经典问题的解法,不仅能帮助你在面试中游刃有余,更能培养将复杂问题分解优化的工程思维。记住,好的算法就像精巧的机械表,每个零件都精准配合,最终呈现出简洁优雅的解决方案。