#322. Coin Change
題目描述
給你一個表示不同面額硬幣的整數陣列coins
以及表示金額的整數amount
。
回傳能夠湊成金額的最少硬幣數量。如果硬幣的組合無法湊成金額,回傳-1
。
你可以假設每個面額的硬幣數量都是無限大。
限制條件
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 231 - 1
- 0 <= amount <= 104
解題思路
Brute Force & Memorize
這一題要我們找出使用最少硬幣的組合,所以一開始挑選硬幣的時候應該要從最大的硬幣開始挑選,並且用目標金額減去選擇的硬幣面額。
當選完一個硬幣之後,下一次的選擇仍然會是所有的硬幣組合。
選完硬幣,並減去額度後會有三種結果:
- 金額為負:代表目前挑選的硬幣是不正確的,因此不用理會。
- 金額為零:代表組合順利完成,當前硬幣為最小的組合。
- 金額為正:代表需要繼續挑選硬幣,繼續挑選則會有另外兩種結果。
- 有組合存在:有組合存在,則根據當下的所有的硬幣挑選結果選出最小的組合結果。
- 沒有組合存在:則標示這個組合為錯誤組合。
這一輪的硬幣選擇結果全部都得出並且比較之後,就可以回傳這一輪的硬幣選擇中最小的結果。
public class Solution {
public int CoinChange(int[] coins, int amount) {
if(amount==0) return 0;
Array.Sort(coins);
return PickCoins(coins,amount,new Hashtable());
}
public int PickCoins(int[] coins, int amount, Hashtable done){
// 如果結果已存在,直接回傳結果
if(done.Contains(amount)) return (int) done[amount];
// 計算不同組合的結果
int minComb = Int32.MaxValue;
for(int i=coins.Length-1;i>=0;i--){
int subVal = amount - coins[i];
// 如果挑選硬幣後的結果已存在,與當前輪數的結果比較並且繼續計算其他挑選組合的結果。
if(done.Contains(subVal)){
int res = (int)done[subVal];
if(res!=-1) minComb = Math.Min(minComb,res);
continue;
}
// 如果金額剛好,則當前組合為最小的組合
if(subVal==0){
if(!done.Contains(subVal)) done.Add(subVal,1);
minComb=1;
}
// 如果金額還有多出,則繼續下一輪的挑選,下一輪會得出最小的組合結果,或者是失敗的組合結果。
if(subVal>0){
int comb = PickCoins(coins,subVal,done);
// 存在組合結果,與當前輪數的結果比較並且繼續計算其他挑選組合的結果。
if(comb!=-1){
if(!done.Contains(subVal)) done.Add(subVal,comb+1);
minComb = Math.Min(minComb,comb+1);
}
// 不存在組合結果,將當前金額標記為沒有組合的結果。
if(comb==-1){
if(!done.Contains(subVal)) done.Add(subVal,-1);
}
}
}
// 如果這一輪所有的條件都沒有達成,則本輪的金額為沒有組合的結果。
if(minComb==Int32.MaxValue) return -1;
// 回傳有組合的結果
return minComb;
}
}
複雜度分析:
- 時間複雜度:最差的結果會是[coins.Length^(amount/最小的硬幣面額)],所以時間複雜度為O(m^n)。
- 空間複雜度:Call Stack最差的情況約會是[amount/最小的硬幣面額],而將結果記錄下來最差的情況會是紀錄所有的組合,約等於[0, 目標金額]的數量,因此空間複雜讀為O(n),n為
amount
。
Dynamic Programming
動態規劃的方法利用製作表格的方式來由下往上計算結果。
舉例來說:
- coins = [1,2,5]
- amount = 11
動態規劃表如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
5 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
我們將上面的表拆成以下三張方便理解:
- coins = [1]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
- coins = [1]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 0 | 0 | 1 | 0 | 2 | 0 | 3 | 0 | 4 | 0 | 5 | 0 |
- coins = [1]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
可以看到以硬幣面額1
來說,湊成0~11的之間的組合很好理解,2
和5
也是同理。 但是以2
和5
的表格來看,是沒有辦法滿足0~11所有金額的組合的。
接下來我們將1
和2
組合起來看:
- coins = [1,2]
- amount = 11
動態規劃表的結果如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 0 | 1 | 0 | 2 | 0 | 3 | 0 | 4 | 0 | 5 | 0 |
面額1
可以單獨完成任務,但是面額2
就必須和面額1
組合起來完成任務,因此會變成以下的表格:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
可以看到面額2
沒有辦法完成的單數透過和面額1
組合而完成,同時具備[1,2]組合中的最小組合。
接下來加入面額5
的表格
- coins = [1,2,5]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
同樣的,由於沒有辦法獨立完成所有的金額的組合,因此需要和1
、2
進行組合,表格如下。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
5 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
同理最後加入的面額一樣可以得到最小組合。
如果順序打亂呢?
假設面額並不是按照順序排列,表格如下:
- coins = [5,1,2]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 0 | 1 | 0 | 2 | 0 | 3 | 0 | 4 | 0 | 5 | 0 |
我們依然可以透過由左而右、由上而下傳遞並取最小值來完成組合,表格如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
1 | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 2 | 3 |
2 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
找出規律
透過上面幾個表格的變化,我們可以找到一個規律,每個金額的最小組合會等於:
金額最小組合 = 最小值(當前組合, (當前位置-當前面額) + 1個面額組合 )。
讓我們回顧剛加入面額5
的組合:
- coins = [1,2,5]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
由於面額5無法滿足金額小於5的組合,因此由面額1
、2
的組合向下遞延:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
5 | 0 | 1 | 1 | 2 | 2 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
從金額5
開始,面額的最小組合會是:
- 1 : amount[5] = min(amount[5],amount[5-(coin)5]+1);
- 2 : amount[6] = min(amount[6],amount[6-(coin)5]+1);
- 2 : amount[7] = min(amount[7],amount[7-(coin)5]+1);
- 3 : amount[8] = min(amount[8],amount[8-(coin)5]+1);
- 3 : amount[9] = min(amount[9],amount[9-(coin)5]+1);
- 2 : amount[10] = min(amount[10],amount[10-(coin)5]+1);
- 3 : amount[11] = min(amount[11],amount[11-(coin)5]+1);
亂序的規律也一樣
假設面額並不是按照順序排列,表格如下:
- coins = [5,1,2]
- amount = 11
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 |
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 0 | 1 | 0 | 2 | 0 | 3 | 0 | 4 | 0 | 5 | 0 |
面額1
可以從金額1
開始組合,直到金額5
的位置,金額5
的位置首先由排序第一的5
向下遞延,因此為1,套用上面找出來的規律。
- 1 : amount[5] = min(amount[5],amount[5-(coin)1]+1);
會等於
- 1 : amount[5] = min(1,4+1);
最小組合仍然是1,可以證明規律是正確的。
程式碼如下:
public class Solution {
public int CoinChange(int[] coins, int amount) {
int[] amounts = new int[amount+1];
for(int i=0;i<amounts.Length;i++) amounts[i]=amount+1;
amounts[0] = 0;
foreach(int coin in coins){
for(int i=coin;i<amounts.Length;i++){
amounts[i]= Math.Min(amounts[i],amounts[i-coin]+1);
}
}
return amounts[amount]>amount? -1 : amounts[amount];
}
}
複雜度分析:
- 時間複雜度:時間複雜度為O(m*n)。
- 空間複雜度:空間複雜度為O(n)。
學習重點
- Recursion
- Dynamic Programming