背包问题分为01背包和完全背包。01背包就是有m种物品放入容量为n的背包中,每种物品只能放一次,问可放入背包的最大价值是多少?
二维数组
递归五部曲:
- dp[i][j]代表的是从下标[0-i]的物品中取,放入容量为[j]的背包中的最大价值为dp[i][j]
- 递推公式:对于每一种物品,只有两种情况,一是放入该物品,二是不放入该物品,于是递推公式就为放入该物品和不放入该物品的价值的最大值。dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
- 初始化:由递推公式可以看出,dp[i][j]和上侧和左侧的状态有关,需要初始上侧和左侧,即dp[i][0]=0,dp[0][j]=value[0]
- 遍历顺序:通过画出二维矩阵图,先遍历背包或者先遍历物品都可以
- 打印dp数组
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int M=sc.nextInt();
int N=sc.nextInt();
int[] weight = new int[M];
int[] value = new int[M];
int bagSize = N;
for(int i=0;i<M;i++){
weight[i]=sc.nextInt();
}
for(int i=0;i<M;i++){
value[i]=sc.nextInt();
}
testWeightBagProblem(weight,value,bagSize);
}
/**
* 动态规划获得结果
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight,int[] value,int bagSize){
int[][] dp=new int[weight.length][bagSize+1];
for(int i=weight[0];i<bagSize+1;i++){
dp[0][i]=value[0];
}
for(int i=1;i<weight.length;i++){
for(int j=1;j<bagSize+1;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[weight.length-1][bagSize]);
}
}
滚动数组
在在二维dp数组中,dp[i][j]的状态都和dp[i-1]有关,这里为了节省空间,可以把二维dp数组压缩成一维dp数组,因为总是依赖于二维矩阵中上侧和上左侧的状态,所以直接将上侧的状态拷贝到当前层来,每次遍历一个物品,就把所有的值拷贝到当前物品层来,看着就像是数组在滚动更新一样,所以也叫滚动数组,这里的拷贝操作是怎么实现的呢?
这里先将递推公式写出:dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);从递推公式可以看出,当前层容量为j的背包所能装的最大价值为dp[j],所以dp[j]的最大值,就是放入当前物品i和不放入当前物品i的最大值。不放入当前物品,就是上一层的状态dp[j],因为每遍历完一个物品,开始重新遍历另一个物品,dp[j]已经将上一个物品在所有背包容量状态下的最大价值状态记录下来了。
- dp数组的含义:容量为j的背包所能装的最大价值为dp[j]
- 递推公式:dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
- 初始化:由dp数组的含义可以知道dp[0]=0,而后面的dp[j]需要不断被更新,所以选个最小价值0作为初始值,便于后续更新dp数组
- 遍历顺序:这里只能是先遍历物品,再遍历背包,同时在遍历背包的时候,必须倒序遍历,因为如果正序遍历背包的话,因为dp[j]依赖于dp[i-1],会将dp[i-1]中放过的元素重复加入,不符合01背包的条件。然后如果先遍历背包再遍历物品的话,因为数组是滚动的,从最大容量的背包开始遍历,因为最大容量的背包所能装的价值也是更多的,在递推公式中,dp[j]滚动更新,在背包容量减小时,dp[j]仍然是上一次最大容量背包的价值,所以导致所有的dp值都会是之前最大容量背包的价值。
- 打印dp数组
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int M=sc.nextInt();
int N=sc.nextInt();
int[] weight = new int[M];
int[] value = new int[M];
int bagSize = N;
for(int i=0;i<M;i++){
weight[i]=sc.nextInt();
}
for(int i=0;i<M;i++){
value[i]=sc.nextInt();
}
testWeightBagProblem(weight,value,bagSize);
}
/**
* 动态规划获得结果
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight,int[] value,int bagSize){
int[] dp=new int[bagSize+1];
Arrays.fill(dp,0);
for(int i=0;i<weight.length;i++){
for(int j=bagSize;j>=weight[i];j--){
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
System.out.println(dp[bagSize]);
}
}
这一题使用动态规划的思路来做,因为每个元素只能取一次,同时看数组能不能分成两个等和的子集,这里不用返回子集是多少,只需要进行判断。那么如果将数组总和除以2,如果有一个组合能满足总和除以2,那么另一个组合也满足,那么就是等和子集。这里利用01背包,将问题转换为,给定等和值sum/2作为背包的最大容量同时也作为最大价值,看看数组中有没有元素组合能够满足装满这个背包的最大容量,怎么判断装满?就是看当背包为最大容量时的最大价值是不是sum/2
dp数组的含义:容量为j的背包所能装的最大价值dp[j]
递推公式:dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
初始化:dp[j]初始化全为0
遍历顺序:先物品后背包,同时背包倒序遍历
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
}
if(sum%2!=0) return false;//如果sum为奇数,直接返回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;
}else{
return false;
}
}
}