一、当自动售货机遇见算法难题
想象你站在一台智能售货机前,投入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, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞]
-
动态填充过程:
金额 i 尝试硬币 计算过程 f[i] 1 1 min(∞, f[0]+1) = 1 1 2 1,2 min(∞, f[1]+1=2, f[0]+1=1) 1 3 1,2 min(∞, f[2]+1=2, f[1]+1=2) 2 4 1,2 min(∞, f[3]+1=3, f[2]+1=2) 2 5 1,2,5 min(∞, f[4]+1=3, f[3]+1=3, f[0]+1=1) 1 ... ... ... ... 11 1,2,5 min(∞, f[10]+1=3, f[9]+1=3, f[6]+1=3) 3 -
最终结果:
f[11] = 3
(5+5+1)
4.2 关键点说明
-
状态定义:
f[i]
表示凑出金额i
的最小硬币数 -
边界条件:
f[0] = 0
:零金额需要零个硬币f[other] = Infinity
:初始化为不可达 -
时间复杂度:
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
- 硬币数组为空时的特殊处理
- 存在面额大于目标金额的硬币
七、从算法到架构的思考
在支付系统的零钱找零模块中,这种算法可以:
- 预计算常见金额的最优解
- 结合缓存机制加速响应
- 动态调整硬币库存状态
- 生成多种找零方案供选择
当你在自动售货机前等待找零时,背后可能正运行着类似的动态规划算法。理解这个经典问题的解法,不仅能帮助你在面试中游刃有余,更能培养将复杂问题分解优化的工程思维。记住,好的算法就像精巧的机械表,每个零件都精准配合,最终呈现出简洁优雅的解决方案。