1. 01背包问题
卡码
求背包装满时最大价值
背包问题只用掌握01背包、完全背包即可,最多加上一个多重背包。
动规五部曲
- 确定dp数组以及下标的含义
i表示将物品i放进背包,j表示背包容量。dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 确定递推公式
求一个状态的价值有两种方式:拿该物品,不拿该物品,也就是说dp[i][j]是由两个方向决定的。
以dp[1][4]举例。如果不拿该物品,推导方向如下,是由上一个状态dp[i-1][j]决定的。
如果拿物品1,则要先预留出物品1的空间,获得那个状态下(dp[0][1])背包的总价值,再加上物品1的价值,推导方向为:
以上过程,抽象化如下:
不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。
放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
;
- dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。首先背包容量为0时dp[i][0]一定为0。然后根据递推公式可得当前i是由i-1那一行推导得来的,那么i=0时一定要初始化,即dp[0][j]。当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
对于其他值,只要初始化的足够小让递推过程中可以被覆盖即可。如果题中给的都是非负数那么可初始化为0。 - 确定遍历顺序
先遍历物品,再遍历背包重量。递推公式中,dp[i][j]都是从左上角的值求出来的,那么先遍历背包重量也可以,不影响递推。 - 举例推导dp数组
如果不理解可以自己模拟一下递推过程。
import java.util.Scanner;
public class Main{
public static void main (String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int bagweight = sc.nextInt();
int[] weight = new int[n];
int[] value = new int[n];
for(int i = 0; i < n; i++){
weight[i] = sc.nextInt();
}
for(int i = 0; i < n; i++){
value[i] = sc.nextInt();
}
int[][] dp = new int[n][bagweight + 1];
for(int i = weight[0]; i <= bagweight; i++){
dp[0][i] = value[0];
}
for(int i = 1; i < n; i++){
for(int j = 0; j <= bagweight; j++){
if(j < weight[i]){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
System.out.println(dp[n - 1][bagweight]);
}
}
01背包用滚动数组(一维数组)解决
观察递推公式,每一行的值都是由上一行推导出来的,而且都是求最大值,最终结果最大值一定是右下角。那么可以将上一行的内容复制到本行,然后直接在本行进行推导,即:
dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。去掉dp[i]这个维度,那么递推公式就变成:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
依然是动规五部曲,dp[j]的定义是容量为j的背包最大价值。初始化时dp[0]一定是0,其余元素仍然保持最小即可。注意遍历顺序,在遍历背包重量时要从大到小遍历。因为递推公式要用到左边的值,从大到小遍历就可以保证递推时仍然使用的是上一层的数据(即dp[i-1][j]),否则就会多次使用前面的值。可以自己手写一遍,就会发现前面的物品被多次装进背包,不符合01背包的要求。
import java.util.Scanner;
public class Main{
public static void main (String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int bagweight = sc.nextInt();
int[] weight = new int[n];
int[] value = new int[n];
for(int i = 0; i < n; i++){
weight[i] = sc.nextInt();
}
for(int i = 0; i < n; i++){
value[i] = sc.nextInt();
}
int[] dp = new int[bagweight + 1];
for(int i = 0; i < n; i++){
for(int j = bagweight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
System.out.println(dp[bagweight]);
}
}
2. 分割等和子集
力扣
背包问题不仅可以求最大价值,也可以求一个背包能不能装满
要求数组中是否有某些元素加起来刚好等于sum/2,可以用背包问题模拟。每个元素就是一个物品,重量就等于价值当物品装满时,如果最大价值刚好等于重量,即dp[target]==target时,说明正好可以装满。
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int i = 0; i < n; i++){
sum += nums[i];
}
if(sum % 2 != 0) return false;
int target = sum/2;
int[] dp = new int[target + 1];
for(int i = 0; i < n; i++){
for(int j = target; j >= nums[i]; j--){
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}