动态规划:322. 零钱兑换(完整思路过程)

题目
322. 零钱兑换

给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
你可以认为每种硬币的数量是无限的。
来源:力扣(LeetCode)

题目特征:
  1. 不同面额的数组:int[] coins;总金额:int amount
  2. 不同面额的硬币数量是无限的。
思路:动态规划
  • 定义:dp[i]:凑成总金额 i 需要的最少硬币数
  • 状态转移方程:
dp[i] = min (dp[i-c]) + 1  // `c` in the array `coins`
代码 1.0(Java)
public int coinChange(int[] coins, int amount) {
    if (amount == 0) return 0;

    int[] dp = new int[amount + 1];  // `dp[amount]` is valid
    Arrays.fill(dp, -1);  // 初识状态:(对应题干)没有任何一种硬币组合能组成总金额 i,则为`-1`
    
    dp[0] = 0;
    /** 
    * 这步是为了后面 dp[i] = min + 1; 也能够处理 i == c 的情况。
	* 其实,删除这步也可以,在 for循环 中分开考虑:
      if (i - c == 0) dp[i] = 1;
      if (i - c > 0) ...
    */

    for (int i = 1; i <= amount; i++) {  // i 直接从 1 开始 是因为 amount == 0 的情况在最开始已经考虑过了
        int min = Integer.MAX_VALUE;
        for (int c : coins) {
            if (i - c >= 0 && dp[i - c] != -1) min = Math.min(min, dp[i - c]);
        	// else dp[i] 保持 -1,因为i < c <=> 总金额 i < 已有的硬币额 c)
        }
        if (min != Integer.MAX_VALUE) dp[i] = min + 1;
    }
    return dp[amount];
}
代码 1.0 主体的 for 循环:

非常好地体现了 dp[i] = min (dp[i-c]) + 1:先在 dp[i-c] 中找到最小值,然后让 dp[i] = min + 1

其实:

dp[i] = min (dp[i - c]) + 1 = min (dp[i-c] + 1)

所以我们可以省去变量 min

for (int i = 1; i <= amount; i++) {
    int dp[i] = Integer.MAX_VALUE;
    for (int c : coins) {
        if (i - c >= 0 && dp[i - c] != -1) dp[i] = Math.min(dp[i], dp[i - c] + 1);  // `i-c > -1` => `dp[i-c]` is valid
    }
    if (dp[i] != Integer.MAX_VALUE) dp[i] = -1;
}

发现这样写代码过于繁琐:

  1. 因为需要找出最小值,所以最开始要给 min 或者 dp[i] 赋上最大值;
  2. 在找最小值时,dp[i-c] = -1 的项是不能参与的:dp[i-c] = -1 表示 没有任何一种硬币组合能组成总金额 i-c,自然也就不能通过 (i-c) + c 的方式凑出 i 了;
  3. 最后,需要判断 mindp[i] 的情况。

如果 数组 dp 的每个元素的初值设为最大值 Integer.MAX_VALUE 可以很好的简化代码:

代码 2.0:
public int coinChange(int[] coins, int amount) {
    if (amount == 0) return 0;
    
    int[] dp = new int[amount+1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = 0;

    for (int i = 1; i<=amount; i++) {
        for (int c : coins) {
            if (i-c >= 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
            //else dp[i] 保持最大值不变
        }
    }

    return dp[amount] == amount+1 ? -1 : dp[amount];
}

如果不想记忆 dp[0] = 0;,可以像 1.0代码 中注释中说的那样:在循环中将 if (i - c == 0) dp[i] = 1; 单独考虑:

	...
    // dp[0] = 0;

    for (int i = 1; i <= amount; i++) {
        for (int c : coins) {
        	if (i - c == 0) dp[i] = 1; // 补上。这样也更好理解。
            if (i - c > 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
        }
    }
    ...

当然,最大值不一定非要暴力地设成 Integer.MAX_VALUE,最大值也可以是:amount + 1

因为:coins[i] >= 1,所以:最多需要 amount 个硬币。

由于 amount + 1在代码中出现频率较高(如:int[] dp = new int[amount+1];i <= amount 其实可以写成 i < amount + 1),所以在 LeetCode官方题解 中,用变量 maxamount + 1 存起来了,所以代码还可以写成:

public int coinChange(int[] coins, int amount) {
    int max = amount + 1;
    int[] dp = new int[max];
    Arrays.fill(dp, max);
    dp[0] = 0;
    for (int i = 1; i < max; i++) {
        for (int c : coins) {
            if (i-c >= 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
        }
    }
    return dp[amount] == max ? -1 : dp[amount];
}

最后,因为本题中 for 循环的顺序对结果没有影响,所以可以将 内外层循环交换位置并设置 i 的起始值 以减少对一些情况的判断(原循环中,当 i 小于 最小的硬币额度 时,内层循环没有必要再一一遍历硬币额度了):

public int coinChange(int[] coins, int amount) {
    int max = amount + 1;
    int[] dp = new int[max];
    Arrays.fill(dp, max);
    dp[0] = 0;
    for (int c : coins) {
    	for (int i = c; i < max; i++) {
            if (i-c >= 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
        }
    }
    return dp[amount] == max ? -1 : dp[amount];
}
总结
  1. 将数组 dp 元素的初值赋上最大值,在最后判断 dp[amount] == max
  2. 记住 dp[0] = 0。记不住的话在 for 循环里单独考虑 if (i == c) dp[i] = 1;

你可能感兴趣的:(LeedCode,动态规划,算法,java,leetcode)