典型的背包问题分为四类:
组合背包:有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。
模板:
https://leetcode-cn.com/problems/target-sum/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-wa5r/
https://leetcode-cn.com/problems/word-break/solution/yi-tao-kuang-jia-jie-jue-bei-bao-wen-ti-kchg9/
参考自背包九讲:
https://blog.csdn.net/yandaoqiusheng/article/details/84782655
总结一下:
背包分类的模板:
01背包:外循环nums,内循环target,target倒序且target>weight[i]
完全背包:外循环nums,内循环target,target正序且target>weight[i]
多重背包:外循环nums,内循环target,内内循环k,target倒序且target>weight[i](我不明白为什么上面的连接里面外层是target,内层是nums,有懂的同学可不可以评论区解释一下)
组合背包:需要三重循环:外循环nums,内部两层循环,根据题目的要求转化为以上三种背包类型的模板
问题分类的模板:
1、最值问题:dp[i] = max/min(dp[i], dp[i-nums]+nums)或dp[i] = max/min(dp[i], dp[i-num]+1);
2、组合问题:dp[i]=dp[i]+dp[i-num];
3、存在问题(bool):dp[i]=dp[i]||dp[i-num];
01背包:
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
for (int j = capacity; j >= weight[i]; j--) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
System.out.println(v[capacity]);
}
}
完全背包:
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
for (int j = weight[i]; j <= capacity; j++) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
System.out.println(v[capacity]);
}
}
多重背包(类似01背包):
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0, 6, 2, 3, 5};
//个体价值
int[] values = {0, 23, 12, 10, 1};
//第i种物品最多有p[i]件可用
int[] p = {0, 1, 2, 3, 4};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
//这是未优化的版本:
for (int i = 1; i < numbers + 1; i++) {
for (int j = capacity; j >= weight[i]; j--) {
for (int k = 0; k <= p[i] && k * weight[i] <= j; k++) {
v[j] = Math.max(v[j], v[j - weight[i] * k] + values[i] * k);
}
}
}
System.out.println(v[capacity]);
}
}
组合背包:
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0, 6, 2, 3, 5};
//个体价值
int[] values = {0, 23, 12, 10, 1};
//第i种物品最多有p[i]件可用
int[] p = {0, 1, 2, 3, 4};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i <= numbers; i++) {
//代表每个物品都有无穷个,所以是完全背包
if (p[i] == 0) {
for (int j = weight[i]; j <= capacity; j++) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
} else {
//代表每个物品为有限个,所以是01背包或者多重背包
//注意这里的for循环,k在外面,与多重背包不同,多重背包的k在里面
//把问题从“这个物品能取多少次”转化为了“有多少个这样的物品”
//多以转移方程不用乘以k了。
for (int k = 1; k <= p[i]; k++) {
for (int j = capacity; j >= weight[i]; j--) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
}
}
System.out.println(v[capacity]);
}
}
--------------------------------------------------------------------我是分割线----------------------------------------------------------------------------
下面的不是很全。
01背包:
题解见:https://blog.csdn.net/Rex_WUST/article/details/89336939
二维简化版:
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j,前i个物品最佳组合对应的价值
int[][] v = new int[numbers + 1][capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
for (int j = 1; j < capacity + 1; j++) {
if (j < weight[i]) {
v[i][j] = v[i - 1][j];
} else {
//假设不装最后一个体积为5的物品,则前三个物品:6,2,3装入容量为j=11的背包中,价值为23+12+10=45
//装了最后一个体积为5的物品,则剩余容量为j-w[5]=11-5=6,装剩余前三个物品:6,2,3,价值最大为23+1=24
//所以,装了还不如不装。
v[i][j] = Math.max(v[i - 1][j], v[i - 1][j - weight[i]] + values[i]);
}
}
}
System.out.println(v[numbers][capacity]);
}
}
二维完整版:
package test;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
/* 1.读取数据 */
int number = sc.nextInt(); // 物品的数量
// 注意:我们声明数组的长度为"n+1",并另score[0]和time[0]等于0。
// 从而使得 数组的下标,对应于题目的序号。即score[1]对应于第一题的分数,time[1]对应于第一题的时间
int[] weight = new int[number + 1]; // {0,2,3,4,5} 每个物品对应的重量
int[] value = new int[number + 1]; // {0,3,4,5,6} 每个物品对应的价值
weight[0] = 0;
for (int i = 1; i < number + 1; i++) {
weight[i] = sc.nextInt();
}
value[0] = 0;
for (int i = 1; i < number + 1; i++) {
value[i] = sc.nextInt();
}
int capacity = sc.nextInt(); // 背包容量
/* 2.求解01背包问题 */
int[][] v = new int[number + 1][capacity + 1];// 声明动态规划表.其中v[i][j]对应于:当前有i个物品可选,并且当前背包的容量为j时,我们能得到的最大价值
// 填动态规划表。当前有i个物品可选,并且当前背包的容量为j。
for (int i = 0; i < number + 1; i++) {
for (int j = 0; j < capacity + 1; j++) {
if (i == 0) {
v[i][j] = 0; // 边界情况:若只有0道题目可以选做,那只能得到0分。所以令V(0,j)=0
} else if (j == 0) {
v[i][j] = 0; // 边界情况:若只有0分钟的考试时间,那也只能得0分。所以令V(i,0)=0
} else {
if (j < weight[i]) {
v[i][j] = v[i - 1][j];// 包的容量比当前该物品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
} else {
v[i][j] = Math.max(v[i - 1][j], v[i - 1][j - weight[i]] + value[i]);// 还有足够的容量可以装当前该物品,但装了当前物品也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。
}
}
}
}
System.out.println();
System.out.println("动态规划表如下:");
for (int i = 0; i < number + 1; i++) {
for (int j = 0; j < capacity + 1; j++) {
System.out.print(v[i][j] + "\t");
}
System.out.println();
}
System.out.println("背包内最大的物品价值总和为:" + v[number][capacity]);// 有number个物品可选,且背包的容量为capacity的情况下,能装入背包的最大价值
/* 3.价值最大时,包内装入了哪些物品? */
int[] item = new int[number + 1];// 下标i对应的物品若被选中,设置值为1
Arrays.fill(item, 0);// 将数组item的所有元素初始化为0
// 从最优解,倒推回去找
int j = capacity;
for (int i = number; i > 0; i--) {
if (v[i][j] > v[i - 1][j]) {// 在最优解中,v[i][j]>v[i-1][j]说明选择了第i个商品
item[i] = 1;
j = j - weight[i];
}
}
System.out.print("包内物品的编号为:");
for (int i = 0; i < number + 1; i++) {
if (item[i] == 1) {
System.out.print(i + " ");
}
}
System.out.println("----------------------------");
}
}
}
优化空间复杂度版本:(这个是最终版,好多题都是这个模板)
关于初始状态:
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
for (int j = capacity; j >= weight[i]; j--) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
System.out.println(v[capacity]);
}
}
一个常数优化
前面的代码中有f o r ( j = V . . . w [ i ] ) for(j=V...w[i])for(j=V...w[i]),还可以将这个循环的下限进行改进。
由于只需要最后f [ j ] f[j]f[j]的值,倒推前一个物品,其实只要知道f [ j − w [ n ] ] f[j-w[n]]f[j−w[n]]即可。以此类推,对以第j jj个背包,其实只需要知道到f [ j − s u m ( w [ j . . . n ] ) ] f[j-sum({w[j...n]})]f[j−sum(w[j...n])]即可,即代码可以改成
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
int numSum = 0;
for (int k = i + 1; k < numbers + 1; k++) {
numSum = weight[k] + numSum;
}
int bound = Math.max(capacity - numSum, weight[i]);
for (int j = capacity; j >= bound; j--) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
System.out.println(v[capacity]);
}
}
完全背包:
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j,前i个物品最佳组合对应的价值
int[][] v = new int[numbers + 1][capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
for (int j = 1; j < capacity + 1; j++) {
if (j < weight[i]) {
v[i][j] = v[i - 1][j];
} else {
//完全背包就是变成了v[i][j-weight[i]]+values[i],
//在第i个物品上面判断是不是需要再往上面添加物品i
v[i][j] = Math.max(v[i - 1][j], v[i][j - weight[i]] + values[i]);
}
}
}
System.out.println(v[numbers][capacity]);
}
}
优化空间版本:
import java.util.*;
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0,6,2,3,5};
//个体价值
int[] values = {0,23,12,10,1};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i < numbers + 1; i++) {
for (int j = weight[i]; j <= capacity; j++) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
System.out.println(v[capacity]);
}
}
多重背包:
题目
有N种物品和一个容量为V的背包。第i种物品最多有p[i]件可用,每件费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
转换为01背包:
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0, 6, 2, 3, 5};
//个体价值
int[] values = {0, 23, 12, 10, 1};
//第i种物品最多有p[i]件可用
int[] p = {0, 1, 2, 3, 4};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
//这是未优化的版本:
for (int i = 1; i < numbers + 1; i++) {
for (int j = capacity; j >= weight[i]; j--) {
for (int k = 0; k <= p[i] && k * weight[i] <= j; k++) {
v[j] = Math.max(v[j], v[j - weight[i] * k] + values[i] * k);
}
}
}
// 这是优化之后的版本(回头看看背包九讲...):
// for (int i = 1; i < numbers + 1; i++) {
// int num = Math.min(p[i], capacity / weight[i]);
// for (int k = 1; num > 0; k <<= 1) {
// if (k > num) {
// k = num;
// }
// num = num - k;
// for (int j = capacity; j >= weight[i] * k; j--) {
// v[j] = Math.max(v[j], v[j - weight[i] * k] + values[i] * k);
// }
// }
// }
System.out.println(v[capacity]);
}
}
组合背包:
问题
如果将前面三个背包混合起来,也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包),应该怎么求解呢?
01背包与完全背包的混合:
考虑到在01背包和完全背包中给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。
再加上多重背包:
如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。但如果不考虑超过NOIP范围的算法的话,用多重背包中将每个这类物品分成O(log(p[i]))个01背包的物品的方法也已经很优了。当然,更清晰的写法是调用我们前面给出的三个相关过程。代码:
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
//物品个数
int numbers = 4;
//背包容量
int capacity = 11;
//个体容量
int[] weight = {0, 6, 2, 3, 5};
//个体价值
int[] values = {0, 23, 12, 10, 1};
//第i种物品最多有p[i]件可用
int[] p = {0, 1, 2, 3, 4};
//当前背包容量 j的物品最佳组合对应的价值
int[] v = new int[capacity + 1];
for (int i = 1; i <= numbers; i++) {
//代表每个物品都有无穷个,所以是完全背包
if (p[i] == 0) {
for (int j = weight[i]; j <= capacity; j++) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
} else {
//代表每个物品为有限个,所以是01背包或者多重背包
//注意这里的for循环,k在外面,与多重背包不同,多重背包的k在里面
//把问题从“这个物品能取多少次”转化为了“有多少个这样的物品”
//多以转移方程不用乘以k了。
for (int k = 1; k <= p[i]; k++) {
for (int j = capacity; j >= weight[i]; j--) {
v[j] = Math.max(v[j], v[j - weight[i]] + values[i]);
}
}
}
}
System.out.println(v[capacity]);
}
}