1. 问题背景与核心挑战
零钱兑换问题是算法领域中经典的动态规划应用题,也是LeetCode上高频出现的面试题型(编号322)。该问题的基本描述是:给定不同面额的硬币和一个总金额,计算凑成该金额所需的最少硬币数量。如果没有任何一种硬币组合能组成该金额,则返回-1。
举个例子,假设硬币面额为[1, 2, 5],总金额为11,那么最优解是3(5+5+1)。这个问题看似简单,但实际涉及多个关键算法思维:
- 贪心算法的局限性(例如面额为[1, 3, 4]时,总金额6用贪心得到4+1+1并非最优解3+3)
- 动态规划的状态转移方程构建
- 边界条件处理(如金额为0或无法兑换的情况)
2. 动态规划解法详解
2.1 基础DP思路
最直接的动态规划解法是创建一个长度为amount+1的数组dp,其中dp[i]表示凑成金额i所需的最少硬币数。初始化时,dp[0]=0(金额为0时不需要硬币),其他位置初始化为一个极大值(如amount+1)。
状态转移方程为:
code复制dp[i] = min(dp[i], dp[i - coin] + 1) 对于所有coin in coins
具体实现步骤:
- 初始化dp数组
- 遍历1到amount的每个金额
- 对于每个金额,遍历所有硬币面额
- 如果当前硬币面额小于等于当前金额,则更新dp值
- 最后检查dp[amount]是否被更新过
python复制def coinChange(coins, amount):
dp = [amount + 1] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] <= amount else -1
2.2 时间复杂度优化
上述解法的时间复杂度是O(amount * n),其中n是硬币种类数。在实际面试中,可以讨论以下优化方向:
- 提前对coins排序,当coin > i时可以提前终止内层循环
- 使用备忘录法的递归实现(虽然时间复杂度相同,但可能更直观)
- 对于特定面额组合的数学解法(如全是倍数关系时可用贪心)
3. 边界条件与特殊测试用例
3.1 必须考虑的边界情况
- 金额为0时应返回0
- 无法兑换的情况(如coins=[2], amount=3)
- 包含面额1的硬币时一定有解(但可能不是最优)
- 超大金额时的性能问题(需注意题目约束)
3.2 典型测试用例示例
python复制测试用例1:
输入:coins = [1, 2, 5], amount = 11
输出:3 (5+5+1)
测试用例2:
输入:coins = [2], amount = 3
输出:-1
测试用例3:
输入:coins = [1], amount = 0
输出:0
测试用例4:
输入:coins = [186, 419, 83, 408], amount = 6249
输出:20
4. 算法扩展与变种问题
4.1 计算兑换方式总数(LeetCode 518)
类似问题但要求计算所有可能的组合方式数。此时状态转移方程变为:
code复制dp[i] += dp[i - coin]
需要特别注意初始化方式(dp[0]=1)和遍历顺序(先coin后amount以避免重复计算)
4.2 最少硬币数的具体组合
如果需要输出具体的硬币组合而不仅是数量,可以额外维护一个记录最后使用的硬币的数组:
python复制def coinChangeWithCombination(coins, amount):
dp = [amount + 1] * (amount + 1)
last_coin = [-1] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i and dp[i] > dp[i - coin] + 1:
dp[i] = dp[i - coin] + 1
last_coin[i] = coin
if dp[amount] > amount:
return -1, []
# 回溯组合
res = []
remaining = amount
while remaining > 0:
res.append(last_coin[remaining])
remaining -= last_coin[remaining]
return dp[amount], sorted(res, reverse=True)
5. 面试实战技巧
5.1 白板编码时的注意事项
- 先明确问题要求(最少数量还是所有组合)
- 讨论输入范围(硬币面额是否包含负数或0)
- 从暴力解法开始,逐步优化到DP
- 手写测试用例验证边界条件
- 解释时间/空间复杂度时要具体
5.2 常见follow-up问题
- 如果硬币数量无限vs有限(背包问题变种)
- 如何处理浮点金额(转换为整数处理)
- 内存优化(滚动数组)
- 并行计算可能性(分段处理)
关键提示:在面试中,即使知道最优解,也应该展示思考过程。可以先提出贪心解法,证明其不适用性,再过渡到动态规划方案。