有一个背包,容量4磅,现有如下为物品:
物品 | 重量 | 价格 |
---|---|---|
吉他(G) | 1 | 1500 |
音响(S) | 4 | 3000 |
电脑(L) | 3 | 2000 |
在不超出容量,且装入物品不重复的情况下让背包中的价值最大
思路分析:
利用动态规划来解决。
每次遍历到第i个物品,根据w[i],v[i]来确定是否需要将该物品装入背包,即给定n个物品,设v[i],w[i]分别为第i个物品的价值和重量。
C为背包的容量,再令v[i] [j]表示前i个物品中能够装入容量为j的背包中的最大价值,则我们有下面结果:
行是i,列是j
1. val[i][0] = val[0][j] =0; //表示填入表的第一行,第一列都为0
2. w[i]>j -> val[i][j] = val[i-1][j]; //当准备加入新增的商品容量大于当前背包的容量,则直接使用上一单元格的策略
3. j>=w[i] -> val[i][j] = max{val[i-1]][j], val[i-1][j-w[i]]+v[i]}; //当准备加入的新增的商品小于等于当前背包容量
/*装入的方式:
val[i-1][j]: 上一单元格存入的最大价值
v[i]:当前商品的价值
val[i-1][j-w[i]]:装入i-1商品,到剩余空间j-w[i]的最大价值
*/
填表过程:
假设分别存在容量为0,1,2,3,4的背包
物品i\j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | |
吉他(G) | 0 | 1500(G) | 1500(G) | 1500(G) | 1500(G) |
音响(S) | 0 | 1500(G) | 1500(G) | 1500(G) | 3000(S) |
电脑(L) | 0 | 1500(G) | 1500(G) | 2000(L) | 3500(G+L) |
- 假设现在只有吉他(G),不管背包多大,只能放一个吉他
- 假设现在有吉他和音响,由于当背包小于4时,只能放吉他,但是当背包大小为4时音响价值大于吉他,所以正在背包中放入音响。
- 假设现在有吉他,音响和电脑,在背包为3时,电脑价值高于吉他所以放入电脑,而背包为4时,既可以放入吉他和电脑,也可以单独放入音响,G+L>S,因此选择放入吉他和电脑。
解决01背包问题的主要抓手是设计表格,进而设计状态转换方程,一定要注意动态规划的根本在于状态转换,基于某种条件判断,在解决下一个子问题的时候是否需要转换状态。
解决动态规划问题总结:
- 设计初始状态
- 分析题目得到状态条件,写状态转换方程
- 写代码
int[] w = {1, 4, 3}; //背包中装入的物品的重量
int[] v = {1500, 3000, 2000}; //装入物品的价值
int m= 4; //背包承受的重量
二维数组表示
public static void two_diversity(int[] w,int[] v,int m){
/*
行是i,列是j
1. v[i][0] = v[0][j] =0; //表示填入表的第一行,第一列都为0
2. w[i]>j -> v[i][j] = v[i-1][j]; //当准备加入新增的商品容量大于当前背包的容量,则直接使用上一单元格的策略
3. j>=w[i] -> v[i][j] = max{v[i-1]][j], v[i-1][j-w[i]]+v[i]}; //当准备加入的新增的商品小于等于当前背包容量
//装入的方式:
v[i-1][j]: 上一单元格存入的最大值
v[i]:当前商品的价值
v[i-1][j-w[i]]:装入i-1商品,到剩余空间j-w[i]的最大值
*/
int n = v.length; //所有物品的个数
//创建二维数组
//v[i]:当前商品的价值
//w[i]:当前商品的重量
//val[i][j]:当前的前i个物品中能够装入容量为j的背包中的最大价值
int[][] val = new int[n+1][m+1];
//初始化第一行第一列
for (int i = 0;i<val.length;i++){
val[i][0] = 0;
}
for (int j = 0;j<val[0].length;j++){
val[0][j] = 0;
}
//根据公式得到动态规划代码
for (int i = 1;i<val.length;i++){
for (int j = 1;j<val[0].length;j++){
if (w[i-1]>j){
val[i][j] = val[i-1][j];
}else {
//因为i从1开始,所以可以用-1,否则前面是+1
//val[i][j] = Math.max( val[i-1][j], val[i-1][j-w[i-1]]+v[i-1]);
if(val[i-1][j]<val[i-1][j-w[i-1]]+v[i-1]){
val[i][j] = val[i-1][j-w[i-1]]+v[i-1];
path[i][j] = 1;
}else {
val[i][j] = val[i-1][j];
}
}
}
}
//输出
for (int[] ints : val) {
for (int anInt : ints) {
System.out.print(anInt+"\t");
}
System.out.println();
}
}
使用一维数组优化
更新的状态转移方程:
val[i] = max{val[j],val[j-w[i]]+v[i]};
不需要枚举所有的情况
两个优化之处
- 使用一位数组优化01背包问题
- 不保存之前状态,取最大值作为当前填装的最大值直接更新,一维数组的最后一个元素就是背包问题要求价值最大值
因此可以求得重量小于等于背包容纳量的时候背包能存放的最大价值
想要求得重量恰好为背包容量最大值时,只把val[0]初始化为0,其他元素都初始化为-∞,确保所有状态都从val[0]转移过来。
public static void one_diversity(int[] w,int[] v,int m){
//两个优化之处
//1.使用一位数组优化01背包问题
//2.不保存之前状态,取最大值作为当前填装的最大值直接更新,一维数组的最后一个元素就是背包问题要求价值最大值
int n = v.length; //所有物品的个数
int[] val = new int[m+1]; //每一种背包承受重量都对应一个最优解,即当前重量下装入价值最大,故有m+1种状态
//这种方法相当于往空背包中逐个添加物品
for (int i = 0;i<n;i++){
for (int j = m; j >= w[i]; j--){
val[j] = Math.max(val[j],val[j-w[i]]+v[i]);
}
}
System.out.println(Arrays.toString(val));
//取最大值,即val[m]
System.out.println(val[m]);
分析:
- 一维数组优化后,val[j]其实是限定装入重量时此时背包总价值。
- 每次外层循环是对应的是每次尝试将不同的物品装入空背包,然后计算背包承重越来越小的时候存入物品的最大总价值,当第一次外层循环结束时,我们得到一个基础一维数组val[j],为装入只有一种物品时,每种容量背包能容纳的最大价值。
- 内层循环对数组进行更新,在后几次外层循环中,如果当前承重足够装下新加入的物品,则计算装下当前物品时,当前背包价值则取装下该物品时的价值,与当前背包装下当前物品时空余空间再装入其他物品的价值之和的最大值,即: val[i] = max{val[j],val[j-w[i]]+v[i]};
- 于是我们每次循环都是在上一层循环的基础上进行状态转移,即在多添加一种新物品时背包总价值最大为多少。每一次循环更新物品种类后,都会再计算当前物品种类和当前承重情况下的最优解。
- 每次计算承重的内层循环都是倒序的,这是因为我们在使用一维数组的时候会覆盖上一层循环的计算结果,而计算下一状态时却需要上一层计算得到的val数组,如果顺序的话就会因为覆盖而使用当前层数值导致状态转移出错。因此倒序计算可以避免覆盖上一层计算结果。
- 如果在每次外层循环中将val[j]进行打印,则与二维数组的表格一致。
一维二维对比
val[i] = max{val[j],val[j-w[i]]+v[i]};等价于val[i] [j] = max{val[i-1]] [j], val[i-1] [j-w[i]]+v[i]};
使用一维数组可以压缩空间,降低空间复杂度。
二维空间复杂度O(mn)
一维空间复杂度O(m)
01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。