背包问题:
题目描述
小明有一个容量为 V 的背包。
这天他去商场购物,商场一共有 N 件物品,第 i 件物品的体积为 wi,价值为 vi。
小明想知道在购买的物品总体积不超过 V 的情况下所能获得的最大价值为多少,请你帮他算算。
输入描述
输入第 1 行包含两个正整数N,V,表示商场物品的数量和小明的背包容量。
第 2∼N+1 行包含 2 个正整数w,v,表示物品的体积和价值。
1≤N≤102,1≤V≤103,1≤wi,vi≤10^3。
输入输出样例
示例 1
输入
5 20
1 6
2 5
3 8
5 15
3 3
输出
37
二维数组dp[i][j]表示前i个(第1个到第i个)物品装入容量为j的背包中获得的最大价值。
把每个dp[i][j]都看成一个背包:背包的容量为j,装第1~第i个物品。最后得到dp[N][C]就是问题的答案:把N个物品装进容量为C的背包的最大价值。
在计算dp[i][j]的过程中,分为以下两种情况
1.第i个物品的体积比容量j还大,不能装进容量为j的背包。直接继承前i-1个物品容量为j的背包的情况。也就是说dp[i][j]=dp[i-1][j]。
2.第i个物品的体积比容量小,这个时候能装进背包,又分为两种情况:装或者不装。
装第i个物品。从前i-1个物品的情况推测,前i-1个物品是dp[i-1][j]。将第i个物品装进背包后,背包容量减少c[i],价值增加w[i],所以有dp[i][j]=dp[i-1][j-c[i]]+w[i]。(也就是说容量减少为c[i],我们需要找dp[i-1][j-c[i]的最大价值加上当前放入物品的价值)
不装第i个物品,那么最大价值还是dp[i][j]=dp[i-1][j],容量不变找i-1的物品的最大价值。
在两种情况下找最大价值的那一个。
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])
import java.util.Scanner;
public class 背包问题01 {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int N=in.nextInt();
int V=in.nextInt();
int []w=new int[N+1];//物品的体积
int []v=new int[N+1];//物品的价值
for(int i=0;i<N;i++) {
w[i]=in.nextInt();//容量
v[i]=in.nextInt();//价值
}
//dp[i][j]代表从下标为[0-N]的物品里任意取,放进容量为V的背包,价值总和最大是多少
int[][] dp=new int[N][V+1];//v+1表示最大取V 0 1 2 3 4
//初始化
//i应该从w[0]开始,因为容量w[0]之后在V的容量范围中都是够装的
for(int i=w[0];i<=V;i++){
dp[0][i]=v[0];
}
//填充dp数组
for(int i=1;i<N;i++){
//i代表物品个数
for(int j=1;j<=V;j++){
//j代表物品容量
if(w[i]>j){
//如果当前背包容量小于物品的容量,是不放物品
dp[i][j]=dp[i-1][j];//那么i-1个物品的价值就是当前情况最大的价值
}
else {
//如果当前背包容量大于物品的容量
//但是此时分为两种情况
//1.不放物品
//2.放物品 背包容量减少dp[i][j-w[i]],价值增加v[i]
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}
}
System.out.println(dp[N-1][V]);
//打印dp数组
for(int i=0;i<N;i++){
for(int j=0;j<=V;j++){
System.out.print(dp[i][j]+"\t");
}
System.out.println("\n");
}
}
}
滚动数组实现二维数组降一维
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j]。
滚动数组需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
1.确定dp数组的定义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
2.一维dp数组的递推公式
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。
因此递归公式为:
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
3.一维dp数组初始化
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
4.遍历顺序
for(int i = 0; i < weight.size(); i++) {
// 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) {
// 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
第二个for循环倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次。
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
二维数组不需要倒序遍历的原因是因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖。
public static void main(String[] args) {
int[] weight = {
1, 2, 3,5,3};
int[] value = {
6, 5, 8,15,3};
int bagWight = 20;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
砝码称重
样例说明
能称出的 10 种重量是:1、2、3、4、5、6、7、9、10、11。
1 = 1;
2 = 6 − 4 (天平一边放6,另一边放 4);
3 = 4 − 1;
4 = 4;
5 = 6 − 1;
6 = 6;
7 = 1 + 6;
9 = 4 + 6 − 1;
10 = 4 + 6;
11 = 1 + 4 + 6。
1.dp数组的定义
dp[i][j]表示的是前i个砝码是否能够称出重量j。
2.初始化dp数组
dp[0][j]=0,因为没砝码肯定无法进行称重
3.确定dp数组的递推公式
1.当砝码重量等于需要称出的重量时直接称dp[i][j]=true
2.当不等时,此时分为3种情况
1.不加砝码的情况下,则dp[i][j]=dp[i-1][j]
2.放左边:dp[i-1][j-w[i]]表示上一次称砝码放天平左边能不能配出这个重量,如果dp[i-1][j-w[i]]=true,则表示左边能够配出这个j-w[i]的重量,这样再有当前的砝码w[i]放左边自然能够配出重量j。可能会出现j-w[i]为负数的情况(比如容量为3,当砝码重量为4,这个时候的话j-w[i]=3-4=-1),需要加绝对值左边变为1,这样我们将重量为4的砝码放到右边时,右边重量减左边重量能够称出容量3。
3.放右边:dp[i-1][j+w[i]]表示上一次称砝码放天平右边能不能配出这个重量。这个时候如果重量为j + w[i]为true的话,说明上一次找得到j+w[i]的重量,而此时我们又知道当前砝码w[i]的重量,将当前砝码放在左边,这个时候右边重量减左边重量,也能判断容量为j的是否为配成功。
dp数组
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
int count = 0;// 统计能配出的个数
int[] w = new int[101];
boolean[][] dp = new boolean[101][100001];//表示前i个砝码能否称出j重量
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long sum = 0L;
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();// 每个砝码的重量
sum += w[i];// 砝码最大重量
}
for (int i = 1; i <= n; i++) {
// 有多少个砝码
for (int j = 1; j <= sum; j++) {
// 砝码最大重量
if (j == w[i]) {
// 能直接称出砝码的质量(不用放左边或放右边或不放)就直接赋值为1(true)说明可以称
dp[i][j] = true;
} else {
// 分为三种情况1.dp[i-1][j]如果上一次的砝码是1说明可以称出这个重量,我们直接复制下来就行了,表示不放砝码
// 2.dp[i-1][abs(j-w[i])]表示上一次称砝码放天平左边能不能配出这个重量(如果上一次能够称出,后面我们在进行配重的时候就可以直接用上一次的,小问题推大问题)
// 3.dp[i-1][j+w[i]]表示上一次称砝码放天平右边能不能配出这个重量
dp[i][j] = dp[i - 1][j] || dp[i - 1][Math.abs(j - w[i])] || dp[i - 1][j + w[i]];//||一个为true则为true,都为false则为false
}
}
}
for (int i = 1; i <= sum; i++) {
// System.out.print(dp[n][i] + " ");
if (dp[n][i]) {
//前n个砝码能否称出重量i
count++;
}
}
System.out.println(count);
// for (int i = 1; i <= n; i++) {//打印dp数组
// for (int j = 1; j <= sum; j++) {
// System.out.print(dp[i][j]+"\t");
// }
// System.out.println();
// }
sc.close();
}
}