问题
背包问题:有一个背包,容量为4磅 , 现有如下物品:
物品 | 重量 | 价格 |
吉他(G) | 1 | 1500 |
音响(S) | 4 | 3000 |
电脑(L) | 3 | 2000 |
1)要求达到的目标为装入的背包的总价值最大,并且重量不超出
2)要求装入的物品不能重复
思想
动态规划的核心思想是把原来的问题分解成子问题进行求解,也就是分治的思想。但是与分治法不同的地方在于,分治法在子问题和子子问题上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间。
思路
背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用),上面这套题指出装入的物品不能重复,属于01背包。
设w[i]为第i个商品的重量,v[i]为第i个商品的价值,C为背包的容量,物品的总个数为n。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否应该将物品放入到背包当中。令v[i][j]表示在前i个物品中能够装入容量为 j 的背包中的最大价值。
填表过程
假设存在背包容量大小分为1,2,3,4的各种容量的背包(分配容量的规则为最小重量的整数倍):
物品 | 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(LG) |
1.第一行是什么商品都没有,因此不管背包的容量有多大,价值都为0。第一列全为0的原因是如果背包的容量是0的话,那么背包里面就装不下任何的东西。
2.假如现在只有吉他,这时不管背包容量多大,只能放入一把吉他。这样第二行就可以都填入1500。
3.假如有吉他和音箱,背包小于4磅是放不下音箱的,所以前3个只能只放吉他,背包容量是4时只放音箱的价值最大。
4.电脑的重量是3,那么背包容量小于3时只能放把吉他,容量是3时只放电脑的价值最高,容量为4时可以只放音箱,价值为3000,或者放一个电脑加把吉他,价值3500,所以这个价值最高。
公式
- v[i][0] = v[0][i] = 0 //表示表的第一行和第一列
- 当w[i] > j 时,v[i][j] = v[i-1][j] //当准备加入新增的商品容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
- 当 j >=w[i] 时,v[i][j] = max{ v[i-1][ j ] , v[ i-1 ][ j-w[ i ]] + v[ i ] } //当准备加入的新增的商品的容量小于等于当前背包的容量,就需要考虑如何实现装入的方式实现最大价值:
- 如果不拿第i个物品的话,结果v[i-1][j]就是上一个单元格的装入的最大值 。
- 如果拿的话,就是v[ i-1 ] [ j - w[ i ]]+v[ i ] ,其中 v[i] 表示当前商品的价值, v[i-1][j-w[i]] : 装入i1商品,到剩余空间 j-w[i],
既然把i件物品装进背包,那么1,2,3,4.....i-1物品只能占用 j - w[ i ]这么多重量了。这个时候,之前的1,2,3,4......i-1物品在背包容量为(j - w[ i ])下的最大值为v[ i-1 ] [ j - w[ i ]]。此时背包的最大值就是 第i件物品的价值v[i]加上前1,2,3,4....i-1件物品在背包容量为下的最大值v[ i-1 ] [ j - w[ i ]],也就是v[ i-1 ] [ j - w[ i ]]+v[ i ] 。最后选择那个就是取决于拿或者不拿哪一个的价值更高。
验证 i = 1, j = 1 :
w[i] = w[1] = 1
w [1] = 1 j = 1 v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :
v[1][1] = max {v[0][1], v[1] + v[0][1-1]} = max{0, 1500 + 0} = 1500
验证 i = 3;j = 4 :
w[i] = w[3] =3 j = 4
j = 4 >= w[i] = 3 => 4 >= 3
v[3][4] = max {v[2][4], v[3] + v[2][1]} = max{3000, 2000+1500} = 2000+1500
代码实现
public class Packet {
public static void main(String[] args) {
int[] w = {1 , 4, 3}; //物品的重量
int[] val = {1500,3000,2000}; //物品的价值
int m = 4; //背包的容量
int n = val.length; //物品的个数
//创建二维数组,表示在前i个物品中能够装入容量为 j 的背包中的最大价值
int[][] v = new int[n+1][m+1];
//第一行和第一列设置为0
for(int i=0; i < v.length; i++) {
v[i][0] = 0; //将第一列设置为0
}
for(int i=0; i< v[0].length; i++) {
v[0][i] = 0;
}
//为了记录放入商品的情况,定义一个二维数组
int[][] path = new int[n+1][m+1];
//动态规划
for(int i=1; i< v.length; i++) { //不需要再处理第一行,i是从1开始的
for(int j = 1;j < v[0].length; j++) { //不处理第一列
if(w[i-1]>j) { //因为程序是从i开始的,因此原来公式中的w[i]改为w[i-1]
v[i][j] = v[i-1][j];
}else {
//因为i是从1开始的,所以公式调整
//v[i][j] = Math.max(v[i-1][j], val[i-1]+v[i-1][j-w[i-1]]);
//为了记录商品存放到背包的情况,不能简单的直接使用上面的公式
//需要使用if...else...体现
if(v[i-1][j] < val[i-1]+v[i-1][j-w[i-1]]) {
v[i][j] = val[i-1]+v[i-1][j-w[i-1]];
//把当前的情况记录到path
path[i][j] = 1;
}else {
v[i][j] = v[i-1][j];
}
}
}
}
//输出当前数组的值
for(int i=0; i < v.length; i++)
{
for(int j=0; j< v[i].length; j++) {
System.out.print(v[i][j]+" ");
}
System.out.println();
}
int i = path.length - 1; //行的最大下标
int j = path[0].length -1; //列的最大下标
while (i>0 && j>0) { //从path的最大下标开始找
if(path[i][j] == 1) {
System.out.printf("第%d个商品放入背包\n",i);
j = j - w[i-1];
}
i--;
}
}
}