不写标题感觉很丑
当一个问题有很多种可能,有很多种答案的时候,并且这一次选择和上一次选择有联系的时候。我们就需要用到动态规划!
而背包问题就是动态规划中一个典型的问题!
01背包问题
有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值问最多能装入背包的总价值是多大?
0代表不拿,1代表拿
我试着用一些比较能够理解的例子来解决动态规划种的问题。
现在我要搭飞机去找女朋友,并且想带一行李箱的南京土特产给她。但是由于买的特价机票,没有免费的行李额不能托运。我寻思着买个托运费用那不得几百块,不如先带一些有价值有意义的礼物给她,用书包装上然后带上飞机,其他带不了的礼物等以后有机会再送给她。但是现在有一个问题,我现在有以下礼物:
但是我的背包装得下8kg的物品,怎么才能保证带给女朋友的礼物价值最大呢?(别问我为什么要带电脑,谁也无法阻止我学习!!!)
这个也就是01背包问题的现实版!我把这些数据依次抽象数组:
//书包bag的最大承重量
int bag = 8;
//物品的个数number
int num = 5;
//物品的价值value
int[] v = {75,100,50,200,150};
//物品的重量weigth
int[] w = {5,1,2,7,1}
动态规划,就是一个在递推的过程,我们先来创建个表:
i 代表纵坐标,表示当前物品数量
j 代表横坐标,表示当前背包容量
w[i] 是物品重量的数组
v[i] 是物品价值的数组
创建dp[i][j] 数组,dp数组中 i 表示当前的物品数量,j 表示当前物品容量。dp[i][j] 中放的元素就是物品的价值
int[][] dp = new int[num+1][bag+1]
比如说dp[2][3] 的意思就是当我的背包可以装 3kg 时,目前只有两个物品,分别是南京的盐水鸭、秋天的银杏叶。
在这里解释以下,dp[0][0]表示当前背包啥也放不了,也啥物品都没有的时候。因为动态规划中经常会用到dp[i-1][j-1] 等数据,为了防止越界,我们通常会在原来的基础上加1,这样也便于理解。比如说,我想表示我当前背包容量为5,目前遇到第5个物品,可以直接表示为dp[5][5],如果是不设置(0,0),我们就需要在(0,0)做一些羞羞的事(为什么是羞羞的事?因为我不知道怎么表达,反正就那个意思),我们的表示就为dp[4][4],感觉很难受,有时候会写错。
现在我们来设想以下:
1、我们没有遇到物品时,不管背包容量是多大,都是不能放的,所以来看一下表的处理:
2、当我们遇到第1个物品时南京的盐水鸭的时候,我们发现这死鸭子还挺重,背包容量 < 物品重量,没办法,只有当我们背包容量 >= 物品重量的时候,才能放入东西。来看表的处理:
因为只有一个物品,所以不管容量多大,都只能放一个物品。
3、当我们遇到第二个物品银杏叶时,它的重量只有 1 kg,当我们背包容量为 1 kg时,发现之前并没有放入物品,我们就可以给它放进去。此时表的变化:
来到了关键时刻!!!注意注意注意!
我们发现我们的背包容量为 5 的时候,之前已经放入过东西了,怎么办怎么办怎么办?除非女朋友是吃货,不然怎么都得选择银杏叶对吧,因为银杏叶的"价值"更高,此时我们迎来了第一个转移方程:
if(j == w[i]) {
dp[i][j] = Math.max(dp[i-1][j],v[i]);
}
来看表的变化:
别走!别走!还有!
我们来看当背包容量等于 6 的时候,我们发现都可以放下,也就是说,我不仅可以放盐水鸭,还可以放银杏叶,爱情和面包都有了。此时我们迎来了我们的第二个状态方程:
if(j > w[i]) {
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i]] + v[i]);
}
怎么理解这个 Math.max(dp[i-1][j],dp[i-1][j-w[i]] + v[i])
呢,假如我们不拿当前的银杏叶,那么 此时背包里面的价值就为dp[2-1][6] = 75
,如果我们拿了银杏叶,那么我们就要先得到不拿银杏叶的时候的价值加上银杏叶的价值,也就是dp[i-1][j-w[i]] + v[i]
,看表
4、当我们选择第三个物品时,当书包容量为1时,发现并装不下。怎么办,不装呗。我们迎来了第三个转移方程
if(j < w[i]) {
dp[i][j] = dp[i-1][j];
}
当重量为2时,我们发现之前背包容量为2的时候,我们装了一个银杏叶,所以用到了第一个转移方程
if(j == i) dp[i][j] = Math.max(dp[i-1][j],v[i]);
毫无疑问,银杏叶的价值大于明信片,毕竟银杏叶可是我辛辛苦苦去寝室楼下捡的。接下来的步骤有也是根据这三个转移方程来写的!
所以看表:
最终价值最大的就是350,我准备带上手环和笔记本去找女朋友了!
还好笔记本还在,我刚才写数据的时候害怕笔记本不见了!
下面我们来代码实现:
public static int backPackII(int num, int[] W, int[] V) {
int x = W.length;
int[][] dp = new int[x+1][num+1];
//初始化纵坐标
for(int i = 0;i < x+1;i++) {
dp[i][0] = 0;
}
//初始化横坐标
for(int i = 0;i < num+1;i++) {
dp[0][i] = 0;
}
for(int i = 1;i < x+1;i++) {
for(int j = 1;j < num+1;j++) {
if(j < W[i-1]) {
dp[i][j] = dp[i-1][j];
}else if(j == W[i-1]) {
dp[i][j] = Math.max(dp[i-1][j],V[i-1]);
}else {
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-W[i-1]] + V[i-1]);
}
}
}
return dp[x][num];
}
注意细节,W数组和V数组的取值范围。不然会越界。
我顺便遍历了以下dp数组
可能是因为我理解不够深刻,我觉得这些经典的题得多靠自己去理解,去画图!别人的想法永远是别人的想法,表达出来可能理解也会有差别。
所以加油啊各位!
老规矩,小白,如有错误,多多指正!
注:文章中得到转移方程的时机并不是"合理的",我写转移方程的时候只是为了让我自己和初学者更好理解。比如dp可以设置为一维数组,转移方程还可以简化。但是我自己是初学者,我希望如果有初学者能看到这篇文章不会感觉到很快!