友情支持

如果您觉得这个笔记对您有所帮助,看在D瓜哥码这么多字的辛苦上,请友情支持一下,D瓜哥感激不尽,😜

支付宝

微信

有些打赏的朋友希望可以加个好友,欢迎关注D 瓜哥的微信公众号,这样就可以通过公众号的回复直接给我发信息。

wx jikerizhi

公众号的微信号是: jikerizhi因为众所周知的原因,有时图片加载不出来。 如果图片加载不出来可以直接通过搜索微信号来查找我的公众号。

322. 零钱兑换

给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12

  • 1 <= coins[i] <= 231 - 1

  • 0 <= amount <= 104

思路分析

解题思路

  1. 这种找路径,找方法的题一般可以使用回溯法来解决,回溯法也可以说是树形图法,解题的时候使用类似于树状图的结构,使用 自顶而下 的方法。

  2. 而在回溯法中,如果含有很多的重复的计算的时候,就可以使用记忆化的搜索,将可能出现的重复计算大状态使用一个数组来保存其值,在进行重复的计算的时候,就可以直接的调用数组中的值,较少了不必要的递归。

  3. 使用了记忆化搜索后,一般还可以进行优化,在记忆化搜索的基础上,变成 自底而上 的动态规划。

0322 01
0322 02
F(3) =min(F(3−c1),F(3−c 2),F(3−c3))+1
     =min(F(3−1),F(3−2),F(3−3))+1
     =min(F(2),F(1),F(0))+1
     =min(1,1,0)+1
     =1
0322 03
  • 一刷

  • 二刷(暴力穷举)

  • 二刷(动态规划(莫名其妙解法))

  • 二刷(动态规划)

  • 三刷(动态规划)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
 * Runtime: 91 ms, faster than 6.13% of Java online submissions for Coin Change.
 *
 * Memory Usage: 42.7 MB, less than 5.33% of Java online submissions for Coin Change.
 *
 * Copy from: https://leetcode.com/problems/coin-change/solution/[Coin Change solution - LeetCode]
 */
public int coinChange(int[] coins, int amount) {
    return coinChange(coins, amount, new int[amount]);
}

private int coinChange(int[] coins, int current, int[] count) {
    if (current < 0) {
        return -1;
    }
    if (current == 0) {
        return 0;
    }
    if (count[current - 1] != 0) {
        return count[current - 1];
    }
    int min = Integer.MAX_VALUE;
    for (int coin : coins) {
        int res = coinChange(coins, current - coin, count);
        if (0 <= res && res < min) {
            min = res + 1;
        }
    }
    count[current - 1] = (min == Integer.MAX_VALUE) ? -1 : min;
    return count[current - 1];
}

/**
 * Runtime: 427 ms, faster than 5.00% of Java online submissions for Coin Change.
 *
 * Memory Usage: 115.5 MB, less than 5.33% of Java online submissions for Coin Change.
 */
public int coinChangeDpWithMemo(int[] coins, int amount) {
    int count = dpWithMemo(coins, new HashMap<>(), amount, amount);
    if (count > amount) {
        return -1;
    }
    return count;
}

public int dpWithMemo(int[] coins, Map<Integer, Integer> memo, int amount, int current) {
    if (current == 0) {
        return 0;
    }
    if (current < 0) {
        return amount;
    }

    List<Integer> counts = new ArrayList<>();
    for (int coin : coins) {
        int newCoin = current - coin;
        int count;
        if (memo.containsKey(newCoin)) {
            count = memo.get(newCoin);
        } else {
            count = dpWithMemo(coins, memo, amount, newCoin) + 1;
            memo.put(newCoin, count);
        }
        counts.add(count);
    }
    return Collections.min(counts);
}

/**
 * Time Limit Exceeded
 */
public int coinChangeDp(int[] coins, int amount) {
    int count = dp(coins, amount, amount);
    if (count > amount) {
        return -1;
    }
    return count;
}

private int dp(int[] coins, int amount, int current) {
    if (current == 0) {
        return 0;
    }
    if (current < 0) {
        return amount;
    }

    List<Integer> counts = new ArrayList<>();
    for (int coin : coins) {
        counts.add(dp(coins, amount, current - coin) + 1);
    }
    return Collections.min(counts);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 最笨的方法:暴力穷举,超时
 *
 * @author D瓜哥 · https://www.diguage.com
 * @since 2024-06-27 21:12
 */
public int coinChange(int[] coins, int amount) {
  if (amount < 0) {
    return -1;
  }
  if (amount == 0) {
    return 0;
  }
  int result = Integer.MAX_VALUE;
  for (int coin : coins) {
    int cnt = coinChange(coins, amount - coin);
    if (cnt == -1) {
      continue;
    }
    result = Math.min(result, cnt + 1);
  }
  return result == Integer.MAX_VALUE ? -1 : result;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
 * 参考 左程云《程序员代码面试指南》。完全没看明白。
 *
 * @author D瓜哥 · https://www.diguage.com
 * @since 2024-06-27 21:12
 */
public int coinChange(int[] coins, int amt) {
  if (coins == null || coins.length == 0 || amt < 0) {
    return -1;
  }
  int N = coins.length;
  int[][] dp = new int[N + 1][amt + 1];
  // 设置最后一排的值,除了 dp[N][0] 外,其他都是-1
  for (int i = 1; i <= amt; i++) {
    dp[N][i] = -1;
  }
  for (int i = N - 1; i >= 0; i--) {  // 从底向上计算每一行
    printMatrix(dp);
    for (int rest = 0; rest <= amt; rest++) { // 每一行都从左向右
      dp[i][rest] = -1; // 初始时,先设置 dp[i][rest] 的值无效。这样省去了一次遍历设置每一行的操作
      if (dp[i + 1][rest] != -1) { // 下面的值如果有效
        dp[i][rest] = dp[i + 1][rest]; // 先设置成下面的值
      }
      // 如果坐标的位置不越界且有效
      if (rest - coins[i] >= 0 && dp[i][rest - coins[i]] != -1) {
        if (dp[i][rest] == -1) { // 如果之前下面的值无效
          dp[i][rest] = dp[i][rest - coins[i]] + 1;
        } else { // 说明下面和左边的值都有效,取最小的
          dp[i][rest] = Math.min(dp[i][rest], dp[i][rest - coins[i]] + 1);
        }
      }
    }
  }
  System.out.println("最后结果");
  printMatrix(dp);
  return dp[0][amt];
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 参考 https://leetcode.cn/problems/coin-change/solutions/132979/322-ling-qian-dui-huan-by-leetcode-solution/[322. 零钱兑换 - 官方题解^]
 *
 * @author D瓜哥 · https://www.diguage.com
 * @since 2024-06-27 21:12
 */
public int coinChange(int[] coins, int amt) {
  if (coins == null || coins.length == 0 || amt < 0) {
    return -1;
  }
  int max = amt + 1;
  int[] dp = new int[max];
  Arrays.fill(dp, max);
  dp[0] = 0;
  for (int i = 1; i <= amt; i++) {
    for (int coin : coins) {
      if (coin <= i) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
      }
    }
  }
  return dp[amt] > amt ? -1 : dp[amt];
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * @author D瓜哥 · https://www.diguage.com
 * @since 2025-04-09 11:36:55
 */
public int coinChange(int[] coins, int amount) {
  int max = amount + 1;
  // 1、确定 dp 数组(dp table)以及下标的含义
  //   兑换 i 元钱,最少需要多少个货币
  int[] dp = new int[max];
  // 3、dp 数组如何初始化
  //   由于求最小值,则初始化比金额大一的数
  //   注:初始化为 Integer.MAX_VALUE 时,需要处理溢出问题
  Arrays.fill(dp, max);
  dp[0] = 0;
  // 4、确定遍历顺序,先金额从小到大开始遍历,再逐个货币进行尝试
  //   注:这样遍历,就可以尽可能多次使用大大额货币,比如
  //     f(11) = f(11-5) + 1 = (f(11-5-5) + 1) + 1 = 3
  for (int i = 1; i <= amount; i++) {
    for (int coin : coins) {
      // 2、确定递推公式:f(n+1) = min{f(n+1), f(n) + 1}
      if (coin <= i) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
      }
    }
  }
return dp[amount] >= max ? -1 : dp[amount];
// 原始解法,当做一个对比放在这里吧
//    if (amount == 0) {
//      return -1;
//    }
//    Arrays.sort(coins);
//    int[] dp = new int[amount + 1];
//    Arrays.fill(dp, Integer.MAX_VALUE);
//    dp[0] = 0;
//    for (int coin : coins) {
//      for (int i = 1; i <= amount; i++) {
//        for (int j = 1; j * coin <= i; j++) {
//          int pre = dp[i - j * coin];
//          if (pre < Integer.MAX_VALUE) {
//            dp[i] = Math.min(dp[i], pre + j);
//          }
//        }
//      }
//    }
//    return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}

思考题

  1. 如何把动态规划代码写得更简洁一些?

  2. 如何使用自底向上方法来实现一遍动态规划?