給你一個整數陣列 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 |
1 | 1 | 1(使用一個面值為 1 的硬幣) |
2 | 2 | 1(使用一個面值為 2 的硬幣) |
3 | 1 + dp[2], 2 + dp[1] | 2(都是最優解) |
4 | 1 + dp[3], 2 + dp[2] | 2(2 + dp[2]) |
5 | 5 | 1(使用一個面值為 5 的硬幣) |
6 | 1 + dp[5], 2 + dp[4], 5 + dp[1] | 2(1 + dp[5] 或者 5 + dp[1]) |
7 | 7 | 1(使用一個面值為 7 的硬幣) |
8 | 1 + dp[7], 2 + dp[6], 5 + dp[3], 7 + dp[1] | 2(1 + dp[7] 或者 7 + dp[1]) |
9 | 1 + dp[8], 2 + dp[7], 5 + dp[4], 7 + dp[2] | 2(2 + dp[7] 或者 7 + dp[2]) |
10 | 10 | 1(使用一個面值為 10 的硬幣) |
11 | 1 + dp[10], 2 + dp[9], 5 + dp[6], 7 + dp[4], 10 + dp[1] | 2(1 + dp[10] 或者 10 + dp[1]) |
12 | 1 + 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]) |
13 | 1 + dp[12], 2 + dp[11], 5 + dp[8], 7 + dp[6], 10 + dp[3] | 3(都是最優解) |
14 | 1 + 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]
}
}
可以看到上面程式碼巢狀了較多 for
和 if
語句塊,本人用 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)