題目:518題
給定不同面額的硬幣和一個總金額。寫出函數來計算可以湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。
分析:
對於這種動態規划問題,我們必須弄清楚這幾個問題:狀態數組的含義、狀態轉移方程、邊界條件以及狀態數組索引的選擇范圍。首先我們來定義一個狀態數組,根據題目要求我們知道最終的目標是要求組成總金額的組合數量,那么使用動態規划的意義就在於通過將大問題划分成子問題從而求最優解,
那么子問題就是在前i種硬幣的選擇范圍下,湊成當前所要求的金額的組合數目。所以狀態轉移數組就是一個二維數組:dp[i][j]。
dp[i][j]:前i中硬幣,在總金額為j的情況下,硬幣的組合數。
然后來分析狀態轉移方程:
對於當前訪問的金幣coin:
如果 coin > j,那么當前金幣不能放入組合,所以dp[i][j] = dp[i-1][j]
如果 coin <= j,那么當前金幣可以考慮放入組合,而且放入幾個也是需要考慮的,所以因為我們要求的是組合數,所以任何組合都要考慮,所以dp[i][j] = sum(dp[i-1][j-k*coin]),k * coin <= j
邊界條件:
(1)有金幣,但是總額為0時,那么組合數應為1,
(2)無金幣,也無總額,組合數也為1;
(3)總額大於0,金幣為無,那么沒有組合能夠湊成總額,所以組合數為0
所以最終的實現代碼如下:
public int change(int amount, int[] coins) { if (amount>0 && coins.length==0) return 0; int n = coins.length; int[][] dp = new int[n+1][amount+1]; dp[0][0] = 1; for (int i = 1; i <= n; i++) { for (int j = 0; j <= amount; j++) { if (coins[i-1] > j) dp[i][j] = dp[i-1][j]; else { for (int k = 0; k * coins[i-1]<= j; k++) { dp[i][j] += dp[i-1][j-k*coins[i-1]]; } } } } return dp[n][amount]; }
優化一:進一步分析dp[i][j],發現它的值依賴於兩種情況,對於第i個金幣,是否加入背包?
(1)不加入,那么dp[i][j] = dp[i-1][j];
(2)加入,那么當前背包容量變成了j-coin,但是由於金幣是無限的所以對於硬幣的選擇范圍依舊是前i個金幣。
所以狀態方程變成了,dp[i][j] = dp[i-1][j] + dp[i][j-coin],j>=coin,對於邊界dp[i][0]=1(有金幣無總額,組合只有一種)。
優化后的代碼如下:
public int change2(int amount, int[] coins) { if (amount>0 && coins.length==0) return 0; int n = coins.length; int[][] dp = new int[n+1][amount+1]; dp[0][0] = 1; for (int i = 0; i <=n ; i++) { dp[i][0] = 1; } for (int i = 1; i <= n; i++) { for (int j = 0; j <= amount; j++) { dp[i][j] = dp[i-1][j]; if (j>=coins[i-1]) dp[i][j] += dp[i][j-coins[i-1]]; } } return dp[n][amount]; }
繼續優化:將狀態轉移數組,變為一維數組,分析可知dp[i][j]只依賴於相同容量的情況下,它在動態規划表格中的上一行的值;或者相同金幣選擇范圍下,加入當前金幣,在剩余容量的情況下的金幣組合值,所以可以只保留金額這一維度,
變為dp[j],表示在總金額為j的情況下,硬幣組合數量。方程為dp[j] = dp[j] + dp[j-coin],j>=coin;邊界條件就是dp[0] = 1;
代碼如下:
public int change3(int amount, int[] coins) { if (amount>0 && coins.length==0) return 0; int n = coins.length; int[] dp = new int[amount+1]; dp[0] = 1; for (int i = 1; i <= n; i++) { for (int j = coins[i-1]; j <= amount; j++) { dp[j] += dp[j-coins[i-1]]; } } return dp[amount]; }
繼續優化,將外圍數組,變成for-each:
public int change4(int amount, int[] coins) { if (amount>0 && coins.length==0) return 0; if (amount==0) return 1; int n = coins.length; int[] dp = new int[amount+1]; dp[0] = 1; for (int coin : coins) { for (int j = coin; j <= amount; j++) { dp[j] += dp[j-coin]; } } return dp[amount]; }
題目:322題
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
分析:
1.定義狀態轉移數組,dp[i][j]表示前i中硬幣,湊夠j所需的最少的硬幣個數
2.狀態轉移方程:對於第i個硬幣,如果它存在多種情況,放和不放,我們要找的就是在總額j的情況下,湊成j的最小值,所以dp[i][j] = min{dp[i-1][j],dp[i][j-coin]+1},
3.邊界值,對於dp[i][0] = 0,即金額為0的情況下,所需最少金幣數量即為0,數組中其他元素的初始值都設為amount+1,因為要找的是最少的金幣數。
代碼如下:
public int coinChange(int[] coins, int amount) { if (amount == 0) return 0; if (coins.length==1 && amount % coins[0] !=0) return -1; int n = coins.length; int[][] dp = new int[n+1][amount+1]; for (int i = 0; i <= n; i++) { Arrays.fill(dp[i],amount+1); } for (int i = 0; i <= n; i++) { dp[i][0] = 0; } for (int i = 1; i <= n; i++) { for (int j = 0; j <= amount; j++) { if (coins[i-1] > j) dp[i][j] = dp[i-1][j]; else { dp[i][j] = Math.min(dp[i-1][j],dp[i][j-coins[i-1]]+1); } } } return dp[n][amount] == amount+1 ? -1 : dp[n][amount]; }
優化:將轉移數組改為一維數組dp[j]表示金額j的情況下,組成j所需最少的硬幣數量。
public static int coinChange2(int[] coins, int amount){ if (coins == null){ return -1; } if (amount == 0){ return 0; } if (coins.length == 1 && amount%coins[0] != 0){ return -1; } int n = coins.length; int[] dp = new int[amount+1]; Arrays.fill(dp,amount+1); dp[0] = 0; for (int i = 0; i < n; i++) { for (int j = 0; j <= amount; j++) { if (j >= coins[i]){ dp[j] = Math.min(dp[j],dp[j-coins[i]]+1); } } } return dp[amount] == amount + 1 ? -1 : dp[amount]; }
做這種完全背包問題的時候,一定要注意,物品是可以無限取的,所以選擇當前物品放入背包以后是不會影響物品的選擇范圍的,這在狀態轉移方程中尤為重要。