完全背包问题
和01背包的唯一区别就是物品可以被重复选取,基于这个特性,完全背包和01背包的代码也有一些小变化。
在01背包中,如果用滚动数组,必须要先遍历物品,再遍历背包,并且遍历背包的时候要从后往前遍历。这两个要求都偶是为了避免出现一个物品被重复选取的情况,但是完全背包是不用排除这种情况的。
所以完全背包问题先遍历物品还是背包都是可以的,背包也可以从前往后遍历。
但是遍历顺序会根据具体的题目要求有所变化
//先遍历物品,再遍历背包 private static void testCompletePack(){ int[] weight = {1, 3, 4}; int[] value = {15, 20, 30}; int bagWeight = 4; int[] dp = new int[bagWeight + 1]; for (int i = 0; i < weight.length; i++){ // 遍历物品 for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量 dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); } } for (int maxValue : dp){ System.out.println(maxValue + " "); } } //先遍历背包,再遍历物品 private static void testCompletePackAnotherWay(){ int[] weight = {1, 3, 4}; int[] value = {15, 20, 30}; int bagWeight = 4; int[] dp = new int[bagWeight + 1]; for (int i = 1; i <= bagWeight; i++){ // 遍历背包容量 for (int j = 0; j < weight.length; j++){ // 遍历物品 if (i - weight[j] >= 0){ dp[i] = Math.max(dp[i], dp[i - weight[j]] + value[j]); } } } for (int maxValue : dp){ System.out.println(maxValue + " "); } }
注意,本题要求的是组合,而不是排列。例如:amount = 5, coins = [1, 2, 5]。
-
1,2,2
-
2,1,2
如果是组合,这两个算一个,排列的化,这就是两个不同的集合了。
所以我们此处需要在完全背包的代码上做一些新的讲解。此处我直接声明结论
-
如果求的是组合,需要先遍历物品,再遍历背包
-
如果求的是排列,需要先遍历背包,再遍历物品
大家可以举个例子试一下。
class Solution { public int change(int amount, int[] coins) { //完全背包问题,价值和重量都为coins[i]背包容量为amount int[] dp = new int[amount+1]; //初始化 dp[0] = 1; for (int i = 0; i < coins.length; i++) { for (int j = coins[i]; j <= amount ; j++) { dp[j] += dp[j - coins[i]]; } } return dp[amount]; } }
本题就是一个排列的情况,顺序不同的时候是两个结果。我们按照上题的结论来写代码
-
每个数字可以重复选取,所以是完全背包问题,需要从前往后遍历
-
是排列问题,所以要先遍历背包,再遍历物品。
class Solution { public int combinationSum4(int[] nums, int target) { //求的是排列结果,所以应该先遍历背包,再遍历物品 int[] dp = new int[target+1]; dp[0] = 1; for (int j = 1; j <= target; j++) { for (int i = 0; i < nums.length; i++) { if (nums[i] <= j) { dp[j] += dp[j - nums[i]]; } } } return dp[target]; } }
之前用斐波那契数列解答的简单题也可以用排列的方式来解答
class Solution { public int climbStairs(int n) { //也可以按照完全背包,排列的方式来解答 int[] dp = new int[n+1]; dp[0] = 1; for(int i = 0; i <= n; i++) { for(int j = 1; j <=2; j++) { if(j <= i) { dp[i] += dp[i - j]; } } } return dp[n]; } }
根据题意可知,这又是一道完全背包+组合问题,但是求的结果不太一样了。
前集体都是求得集合数量,即有多少种满足要求得组合,而本题则是求满足条件得最小集合。我们得递推公式需要变一下。
之前我们得公式都是dp[j] += dp[j - nums[i]];
,如果求最小值,想当然的就把当前得值和之前得值进行对比,取小的即可。
但是dp[j] += dp[j - nums[i]];
每一次得值都是在上一次得结果上累加而来,所以这个公式目前来说是不能用的。
如果在当前背包容量下,我要装入的物品重量为coins[i],则没装入的时候,背包中的最小集合为dp[j-coins[i]],所以我们在此基础上+1就是新的最小的集合中元素个数。
class Solution { public int coinChange(int[] coins, int amount) { //要求最少的硬币个数 //完全背包,组合问题 int[] dp =new int[amount+1]; //初始化,因为是在原来基础上进行+1,所以dp[0] = 0 Arrays.fill(dp, Integer.MAX_VALUE); dp[0] = 0; for (int i = 0; i < coins.length; i++) { for (int j = coins[i]; j <= amount; j++) { if(dp[j - coins[i]] != Integer.MAX_VALUE) {//当dp[j - coins[i]]为初始值的时候,跳过 dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1); } } } if (dp[amount] == Integer.MAX_VALUE)return -1; else return dp[amount]; } }
本题的难度还是挺高的,在我们熟悉了完全背包问题的解题思路以后,我们可以容易的得出。这是一个完全背包,且求排列的问题。
所以遍历顺序应该是先背包再物品,从前往后遍历。
但是然后就卡住了,不知道接下来怎么写代码了,递推公式也推不出来。
我们来一步一步的慢慢推
-
dp数组含义:dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
-
递推公式:dp数组的特点就是当前状态可以由之前的某一状态推导而来。
当前是dp[i],
-
如果存在一个
len = i-j
长度的单词和s.substrings.substring(i - len, i)
相等 -
并且
dp[i - len]
的值也为true的话,则让dp[i] = true
-
-
初始化:从递推公式中可以看出,
dp[i]
的状态依靠dp[i-len]
是否为true,那么dp[0]就是递推的根基,dp[0]
一定要为true,否则递推下去后面都都是false了。 -
遍历顺序:先背包再物品,从前往后遍历
class Solution { public boolean wordBreak(String s, List<String> wordDict) { //dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。 boolean[] dp = new boolean[s.length()+1]; dp[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < wordDict.size(); j++) { String word = wordDict.get(j); int len = word.length(); if (i >= len && dp[i - len] && word.equals(s.substring(i-len,i))) { dp[i] = true; } } } return dp[s.length()]; } }