01背包
1.dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
2.每个物品只有一个,只有选与不选两个选择,如下,取最大值
- 不选物品i:dp[i][j] = dp[i-1][j];
- 选物品i:dp[i][j] = dp[i-1][j-wight[i]]+val[j];
3.状态转移方程 : dp[i][j] = Math.max(dp[i][j], dp[i-1][j-wight[i]]+val[j] );
4.注意dp[i][j]的初始化
5.遍历顺序:推荐先遍历物品,再遍历背包
优化:
每个取值都依赖于正上方和左上角元素,可以进行空间压缩,从上到下,从右到左。
将其转换成一维dp数组:
dp[j] = Math.max(dp[j], dp[j-weight[i]] + val[i]);
dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
注意:一维dp的遍历顺序与二维dp遍历背包的顺序不一样。
- 二维dp从小到大遍历背包容量
- 一维dp倒序遍历,只为了保证物品i只被放入一次!!!
注意:一维dp 只能先遍历物品,再遍历背包容量!!!
416.分割等和子集
- 背包的体积为sum / 2
- 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素是不可重复放入。
本题中每一个元素的数值既是重量,也是价值。
1.dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
2.递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3.初始化:首先dp[0]一定是0。如果题目给的价值有负数,那么非0下标就要初始化为负无穷。本题 只包含正整数,所以非0下标的元素初始化为0就可以了。
4.先遍历物品;再遍历背包容量,且倒序。
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2 ==1) return false;
int target = sum /2;
int[] dp = new int[target+1];
for(int i = 0; i<nums.length; i++){
for(int j=target; j>=nums[i]; j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
if(dp[target]==target) return true;
}
return dp[target]==target;
}
}
1049.最后一块石头的重量II- 力扣(LeetCode)
本题与上一题是一个意思。只是对dp[target]的处理方式不同。
只要尽量让石头分成重量相同的两堆,相撞之后剩下的石头就会是最小的。
在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int i : stones) {
sum += i;
}
int target = sum >> 1;
//初始化dp数组
int[] dp = new int[target + 1];
for (int i = 0; i < stones.length; i++) {
//采用倒序
for (int j = target; j >= stones[i]; j--) {
//两种情况,要么放,要么不放
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - 2 * dp[target];
}
}
494.目标和. - 力扣(LeetCode)
假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
此时问题就转化为,装满容量为x的背包,有几种方法。
考虑边界:
if ((target + sum) % 2 == 1) return 0; // 此时没有方案
if (abs(target) > sum) return 0; // 此时没有方案
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i = 0; i < nums.length; i++) sum += nums[i];
//如果target的绝对值大于sum,那么是没有方案的
if (Math.abs(target) > sum) return 0;
//如果(target+sum)除以2的余数不为0,也是没有方案的
if ((target + sum) % 2 == 1) return 0;
int bagSize = (target + sum) / 2;
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
}
完全背包
与01背包不同的点在于每种物品有无限件。因此,要从小到大、去正序遍历。
322. 零钱兑换. - 力扣(LeetCode)
1.dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
2.dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
3.首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;
下标非0的元素都应该初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int[] dp = new int[amount+1];
dp[0]=0;
for(int i=1; i<=amount; i++){
dp[i] = max;
}
for(int i=0; i<coins.length; i++){
for(int j=coins[i]; j<=amount; j++){
// 只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
// 如果缺少这个if语句会报错,因为max+1=-max
if(dp[j-coins[i]] != max) dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount] == max ? -1 : dp[amount];
}
}
139.单词拆分. - 力扣(LeetCode)
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
1.dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
2.递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
3.dp[0]一定要为true,否则递推下去后面都都是false了.
4.本题一定是 先遍历 背包,再遍历物品。因为强调物品之间顺序,即排列问题。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> word = new HashSet(wordDict);
boolean[] dp = new boolean[s.length()+1];
dp[0] = true;
for(int i=1; i<= s.length(); i++){
for(int j=0; j<i; j++){
if(dp[j] && word.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
多重背包
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
先遍历物品再遍历背包,作为01背包处理
是01背包里面在加一个for循环遍历一个每种商品的数量。
总结:
01背包:
二维dp时,两个for循环的先后顺序可以颠倒。但一维dp只能先遍历物品,再遍历背包容量,否则每个dp[j]就只会放入一个物品。并且一维dp的背包容量只能倒序遍历,从而保证每个物品最多放入一次。
纯完全背包的两个for循环的先后顺序可以颠倒,因为不会影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for遍历背包,内层for循环遍历物品。