完全背包问题(LeetCode第322、518题)
题目: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]; }
做这种完全背包问题的时候,一定要注意,物品是可以无限取的,所以选择当前物品放入背包以后是不会影响物品的选择范围的,这在状态转移方程中尤为重要。