【LeetCode】322. 零钱兑换

leetcode

 

解题思路:动态规划(自底向上)

该问题属于经典的​​完全背包问题​​变种,核心目标是用​​最少数量的硬币​​凑成目标金额。动态规划是最高效的解法,其核心思想如下:

  1. ​​状态定义​​
    dp[i] 表示凑成金额 i 所需的最少硬币数量。若无法凑成,则值为 -1

  2. ​​状态转移方程​​

    其中需满足 icoin 且 dp[icoin]=1。

  3. ​​边界条件​​

    • dp[0] = 0(金额为0时不需要硬币)
    • 其他值初始化为极大值(表示暂不可达)
  4. ​​算法选择​​
    贪心算法在非规范硬币面值下可能失效(如 coins=[25,21,10,1]amount=63,贪心得6枚,最优解为3枚),因此必须用动态规划保证最优解。


关键步骤

  1. ​​初始化 dp 数组​​
    • 长度为 amount+1dp[0]=0,其余初始化为 amount+1(因为最多只需 amount 枚1元硬币)
  2. ​​遍历金额状态​​
    对每个金额 i(从 1 到 amount),遍历所有硬币面值 coin
    • 若 coin <= i 且 dp[i-coin] 可达(非初始值),则更新:
      dp[i] = min(dp[i], dp[i-coin] + 1)
  3. ​​处理结果​​
    • 若 dp[amount] 仍为初始值 → 返回 -1
    • 否则返回 dp[amount]

​​示例推导​​(coins=[1,2,5]amount=11
| 金额 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|----------|---|---|---|---|---|---|---|---|---|---|---|----|----|
dp[i] | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |


代码实现

func coinChange(coins []int, amount int) int {
    // 边界处理: 金额为0时无需硬币
    if amount == 0 {
        return 0
    }

    // 初始化dp数组, dp[i]表示凑成金额i所需的最少硬币数
    dp := make([]int, amount+1)
    for i := range dp {
        dp[i] = amount + 1 // 初始化为不可达值
    }
    dp[0] = 0 // 金额0不需要硬币

    // 动态规划填表
    for i := 1; i <= amount; i++ {
        for _, coin := range coins {
            // 若硬币面值不超过当前金额, 且减去该面值后的状态可达
            if coin <= i && dp[i-coin] != amount+1 {
                // 更新dp[i]为更优解
                if dp[i-coin]+1 < dp[i] {
                    dp[i] = dp[i-coin] + 1
                }
            }
        }
    }

    // 返回结果
    if dp[amount] > amount {
        return -1 // 无法凑成
    }
    return dp[amount]
}

示例测试

func main() {
    tests := []struct {
        coins  []int
        amount int
        want   int
    }{
        {[]int{1, 2, 5}, 11, 3},       // 11 = 5+5+1
        {[]int{2}, 3, -1},             // 无法凑出3
        {[]int{1}, 0, 0},              // 金额0
        {[]int{25, 21, 10, 1}, 63, 3}, // 最优解:21 * 3(贪心会失效)
    }

    for _, test := range tests {
        res := coinChange(test.coins, test.amount)
        fmt.Printf("coins=%v\tamount=%d\t→ %d (期望: %d)\n",
            test.coins, test.amount, res, test.want)
    }
}

复杂度分析

​​维度​​​​结果​​​​说明​​
时间复杂度 ​​O(n·k)​​ n为金额,k为硬币种类数
空间复杂度 ​​O(n)​​ dp数组长度 = amount+1
适用数据规模 n ≤ 10⁴ 满足LeetCode约束

关键点总结

    1. ​​状态定义精准性​​:
      dp[i] 仅关注硬币数量而非组合方式,避免冗余计算。
    2. ​​初始化技巧​​:
      用 amount+1 表示不可达状态,避免整数溢出(math.MaxInt 可能过大)。
    3. ​​动态规划优势​​:
      • 保证最优解:适用于任意硬币面值组合
      • 高效性:单次遍历即可求解
    4. ​​与贪心对比​​:
      ​​算法​​最优解保证时间复杂度适用场景
      动态规划 O(n·k) 任意面值组合
      贪心算法 ❌(可能失效) O(k) 规范面值(如欧元体系)
    5. ​​边界陷阱​​:
      • 金额为0时返回0(非-1)
      • 硬币面值需为正整数(题目已约束)
posted @ 2025-06-21 12:50  云隙之间  阅读(24)  评论(0)    收藏  举报