算法-回溯法/動態規劃-零錢兌換
1 題目概述
1.1 題目出處
https://leetcode-cn.com/problems/coin-change/
1.2 題目描述
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
示例 1:
輸入: coins = [1, 2, 5], amount = 11
輸出: 3
解釋: 11 = 5 + 5 + 1
示例 2:
輸入: coins = [2], amount = 3
輸出: -1
說明:
你可以認爲每種硬幣的數量是無限的。
2 回溯法
2.1 思路
首先將零錢從小到大排序,然後貪心地優先放面額大的零錢,直到超過目標值就回溯;或等於目標值,就比較當前零錢總數和最小值,如果更小就設爲最小值,然後繼續回溯。
2.2 代碼
class Solution {
public int coinChange(int[] coins, int amount) {
if(coins == null || coins.length == 0){
return -1;
}
Arrays.sort(coins);
return backtrack(coins, amount, 0, 0, coins.length - 1);
}
int min = Integer.MAX_VALUE;
private int backtrack(int[] coins, int amount, int count, int sum, int max){
if(sum > amount){
// 結束條件
return -1;
}
if(sum == amount){
// 結束條件
return count;
}
for(int i = max; i >= 0; i--){
// 選擇
sum += coins[i];
int result = backtrack(coins, amount, count + 1, sum, i);
if(result > -1){
if(result < min){
min = result;
}
}
sum -= coins[i];
}
if(min == Integer.MAX_VALUE){
return -1;
}else{
return min;
}
}
}
2.3 時間複雜度
時間複雜度太高,直接崩了
3 回溯法-剪枝
3.1 思路
前面回溯法,相當於窮舉所有組合,所有超時也是在情理之中。
但由於可能出現以下情況:
對於這個測試用例
[13,12,11,9,61]
33
優先找到:
13 13 6 1
然後可以找到
13 12 6 1 1
還可以找到
13 11 9(最優解)
這說明,我們必須遞歸完這些解,否則可能無法找到全局最優解!
但是我們還是用了剪枝思想,見代碼註釋中的k + cnt < res
。
3.2 代碼
class Solution {
private int min = Integer.MAX_VALUE;
public int coinChange(int[] coins, int amount) {
if(coins == null || coins.length == 0){
return -1;
}
Arrays.sort(coins);
backtrack(coins, amount, 0, coins.length - 1);
return min == Integer.MAX_VALUE ? -1 : min;
}
private void backtrack(int[] coins, int amount, int count, int max){
if(amount == 0){
// 結束條件
min = Math.min(min, count);
return;
}
if(max < 0){
// 結束條件
return;
}
// k + count < min是剪枝關鍵
// 考慮 k + count == min,此時已經不需要再繼續查找,因爲更小面額肯定需要的數量更多,而相等數量的組合已經有了
// 考慮 k + count > min,此時也已經不需要再繼續查找,更小面額組合鈔票數肯定更大於當前 k + count
for(int k = amount / coins[max]; k >= 0 && (k + count < min) ; k--){
backtrack(coins, amount - k * coins[max], count + k, max - 1);
}
}
}
3.3 時間複雜度
這次好很多!
4 動態規劃
4.1 思路
設dp[i]表示金額i的最小硬幣個數,則
dp[i] = Math.min(dp[i-coins[0]] + 1, dp[i-coins[2]] + 1,...,dp[i-coins[n-1]] + 1);
4.2 代碼
class Solution {
private int min = Integer.MAX_VALUE;
public int coinChange(int[] coins, int amount) {
if(coins == null || coins.length == 0 || amount < 0){
return -1;
}
if(amount == 0){
return 0;
}
// 將硬幣從小到大排序,當目標金額小於某個硬幣面值時可直接排除大面額硬幣
Arrays.sort(coins);
// 設dp[i]表示金額i的最小硬幣個數
// 則dp[i] = Math.min(dp[i-coins[0]] + 1, dp[i-coins[2]] + 1,...,dp[i-coins[n-1]] + 1);
int[] dp = new int[amount + 1];
dp[0] = 0;
// 記錄用到的最大硬幣下標
int max = coins.length - 1;
// 更新用到的最大硬幣下標,對一個硬幣剛好能組成的金額進行dp[i]初始化爲1
for(int i = 0; i < coins.length; i++){
if(amount == coins[i]){
return 1;
}
if(coins[i] > amount){
max = i - 1;
break;
}
dp[coins[i]] = 1;
}
// 從1到amout開始動態規劃過程
for(int i = 1; i <= amount; i++){
// 該金額對應的最小硬幣個數
int min = Integer.MAX_VALUE;
for(int j = 0; j <= max; j++){
if(coins[j] > i){
// 如果硬幣面額比當前金額大,就停止當前趟查找最小硬幣個數
break;
}
int tmp = dp[i-coins[j]];
if(tmp == -1){
// 該金額構成肯定不包含當前硬幣
continue;
}
min = Math.min(min, tmp);
}
// min + 1 表示要加上一個硬幣組成當前金額
dp[i] = min == Integer.MAX_VALUE? -1 : (min + 1);
}
return dp[amount];
}
}
4.3 時間複雜度
4.4 空間複雜度
O(amount)