Leetcode 322 零錢兌換

一杯綠茶發表於2021-11-22

給你一個整數陣列 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

示例 4:

輸入:coins = [1], amount = 1
輸出:1

示例 5:

輸入:coins = [1], amount = 2
輸出:2

解題思路

這道題有些人可能會想到貪心法,優先用面值大的硬幣去湊,但是貪心無法獲得最優解。例如下面這個例子:

int[] coins = {1, 2, 5, 7, 10};
int amount = 14;

假如用貪心法,那麼首先肯定是用面值為 10 的硬幣,然後還需兩個面值為 2 的硬幣,總共需要 3 個硬幣。但事實上,只需要兩個面值為 7 的硬幣就能湊出 14 的金額。因此這道題需要用動態規劃的思想。

從下面這個表格可以看出動態規劃的思路,對於需要拼湊的金額 i,遍歷硬幣面值,找到面值比金額低的硬幣 coins[j] ,然後再去看 dp 陣列中 dp[i - coins[j]] 是否有最優解,把所有可能的情況都列出,找最小的即可。整個過程從 1 開始不斷迭代,一直迭代到需要的金額即可。

金額可以湊出的情況最優解
0-0
111(使用一個面值為 1 的硬幣)
221(使用一個面值為 2 的硬幣)
31 + dp[2], 2 + dp[1]2(都是最優解)
41 + dp[3], 2 + dp[2]2(2 + dp[2])
551(使用一個面值為 5 的硬幣)
61 + dp[5], 2 + dp[4], 5 + dp[1]2(1 + dp[5] 或者 5 + dp[1])
771(使用一個面值為 7 的硬幣)
81 + dp[7], 2 + dp[6], 5 + dp[3], 7 + dp[1]2(1 + dp[7] 或者 7 + dp[1])
91 + dp[8], 2 + dp[7], 5 + dp[4], 7 + dp[2]2(2 + dp[7] 或者 7 + dp[2])
10101(使用一個面值為 10 的硬幣)
111 + dp[10], 2 + dp[9], 5 + dp[6], 7 + dp[4], 10 + dp[1]2(1 + dp[10] 或者 10 + dp[1])
121 + dp[11], 2 + dp[10], 5 + dp[7], 7 + dp[5], 10 + dp[2]2(2 + dp[10] 或者 5 + dp[7] 或者 7 + dp[5] 或者 10 + dp[2])
131 + dp[12], 2 + dp[11], 5 + dp[8], 7 + dp[6], 10 + dp[3]3(都是最優解)
141 + dp[13], 2 + dp[12], 5 + dp[9], 7 + dp[7], 10 + dp[4]2(7 + dp[7])

參考程式碼

import java.util.Arrays;

public class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1]; // 初始化 dp 陣列,大小為 amount + 1
        Arrays.fill(dp, -1); // 全部元素初始化為 -1
        dp[0] = 0; // 金額 0 的最優解 dp[0]=0

        // 依次計算 1 至 amount 的最優解
        for (int i = 1; i <= amount; i++) {
            // 對於每個金額 i ,遍歷面值 coins 陣列
            for (int j = 0; j < coins.length; j++) {
                // 需要拼湊的面額 i 比當前面值 coins[j] 大,且金額 i - coins[j] 有最優解
                if (coins[j] <= i && dp[i - coins[j]] != -1) {
                    // 如果當前金額還未計算或者 dp[i] 比當前計算的值大
                    if (dp[i] == -1 || dp[i] > dp[i - coins[j]] + 1) {
                        dp[i] = dp[i - coins[j]] + 1; // 更新 dp[i]
                    }
                }
            }
        }
        return dp[amount]; // 返回金額 amount 的最優解 dp[amount]
    }
}

可以看到上面程式碼巢狀了較多 forif 語句塊,本人用 TypeScript 寫了一個陣列方法的實現:

function coinChange(coins: number[], amount: number) {
  // 建立長度為 amount + 1 ,全部元素為 -1 的陣列
  const dp = Array.from(new Array(amount + 1), () => -1);
  dp[0] = 0;
  // 陣列 forEach 只能從頭開始迭代,不能從中間某個位置開始
  // 這裡還是隻能用 for 迴圈
  for (let i=1; i<=amount; i++) {
    // 遍歷所有面值計算方案
    const schemes = coins
      .filter(coin => (coin <= i) && (dp[i - coin] !== -1))
      .map(coin => 1 + dp[i - coin]);
    // 如果方案存在,則使用方案中的最小值
    if (schemes.length) {
      dp[i] = Math.min(...schemes);
    }
  }
  return dp[amount];
}
用陣列方法幹掉了一個 for 迴圈和兩個 if 判斷,效能肯定不如原來的好,但是提升了語義性,容易理解

時間複雜度:O(M*N)M 為金額大小,N 為硬幣面值數量)
空間複雜度:O(M)

相關文章