欢迎你!
如果让一个初学者直接硬啃0-1背包的代码和公式,那确实有些强人所难。
我们开场不讲公式定理
先入手一个例子
从一个具体问题出发来研究一个0-1背包问题:
【问题描述】一个0-1背包问题
已知一个【背包】,最多可装总重为 5 的物品。
还有3个【物品】
物品 | 价值 | 重量 |
---|---|---|
物品A | 60 | 1 |
物品B | 100 | 2 |
物品C | 120 | 3 |
这三个物品数量都为1
现在要将这些物品装入背包,在不超重的情况下,如何装能够获得最大的价值?求这个最大价值。
【方法1】穷举
最简单的方法就是画一个树型的图来逐个判断每个选择的分支。
基本思路可以是:
“先判断是否装A物品,再判断是否装B物品,再是否装C……”
这里给出这个图:
这个过程简单有效,便于我们理清思路。
基于这个图我们可以快速观察到价值最大的装法。
但每次都画一棵树是不行的,不然随便一个装7、8个物品的同类问题,都会让草稿纸的宽度受到严峻挑战。
我们画一个表格试试看,这个表格最终可以帮你弄懂0-1背包算法的原理
如果手边有草稿纸的话,你也可以一起画一画
【方法2】画表格
由于背包承重为5
,所以表格我们设计5
列,每列表示背包当前最多能装多重的东西,分别包含从一至五的五种情况:1、2、3、4、5
由于有3
个物品要装,所以表格我们设计3
行
暂时先不要深究为什么这么设计,我们一点一点来
画好这个表格之后,我们给它取名为Bag,如下图:
和前面画的那颗树的思路一样,第一步我们先判断是否装物品A的情况,所以第一行的的纵坐标我们命名为“只考虑A”。当前操作的物品为A
表格第一行中每一格的含义代表:“在只考虑装物品A,且背包容量最大为1、2、3、4、5这几种情况下,背包所装物品获得的最大价值”
如下图表格,在只考虑A
,且背包最大容量为1
的情况下,很明显,直接将重量为1、价值为60的物品A装进去,就是当前情况能获得的最大价值了,即Bag[1][1] = 60
同理,只考虑A,在背包承重为2的情况下,也只能放入一个A物品,即Bag[1][2] = 60
。
注意,每个物品数量只有一个,所以在只考虑A的情况下,只能放入一个A物品。
很简单,在只考虑A的情况下,很快就能填完第一行,结果见下图:
表格第二行,我们将“在判断是否装A物品的结果上,进一步考虑是否装B物品”,所以第二行的纵坐标我们命名为“基于A考虑B”
当前物品从A变为B
如下图,在考虑B物品的情况下,对于Bag[2][1]
(即:在考虑B、且背包容量为1时可获得得最大价值),要沿着以下三个基本思路考虑:
1、能不能装入B物品?
2、需不需要装入B物品?
3、如果装入B物品,怎么装能够获得最大价值?
显然在背包承重为1
的时候,无论怎样都不能装入重量为2
的B物品,
那么这种情况下该如何决定Bag[2][1]
的值呢?虽然背包承重为1
时操作不了B物品,但上一轮表格中统计了各个背包承重下基于A物品的最佳结果。所以可以用第一行的结果来代替它。
找到上一轮(也就是表格第一行)中背包承重为1的最大价值——即Bag[1][1]
, 作为Bag[2][1]
的结果。如下图:
可以总结出第一条规律:当前物品装不了,就从“头顶”照抄
接下来是重头戏:Bag[2][2]:基于A考虑B,且背包容量为2的情况下能得到的最大价值
当前背包容量为2
,物品B的重量为2
,意味着我们有条件装入B物体,但是需不需要装入,要用以下几条思路思考:
1、能不能不装入B物品?
2、如果一定要装入B物品,该怎么调整背包内容?
3、上述可能的装法中哪种装法价值最大
结合上面的思路:
1、可以不装入B物品,那么
Bag[2][2]
的一个值可以是Bag[1][2]
(上一轮只操作A物品且背包容量为2的最大价值),即60
2、如果一定要装入B物品,就需要腾出B物品等重量的空间。当前背包承重为2
,为B物品腾出重量为2
的空间后,这个背包的承重就变为0
了,此时我们再将物品B放入背包中,这个背包里现在就只装有B物品了,并且背包的价值为100
。所以Bag[2][2]
的另一个值可以是100
3、比较上述两种情况产生的价值大小,显然“先腾地方、再装入B物品”的方法产生了最大的价值。所以将100
填入Bag[2][2]
分析结果见下表:
同理,我们继续来看Bag[2][3]:基于A考虑B,且背包容量为3的情况下能得到的最大价值
如下图,相信你已经找到一些规律了:
如果不装入B物品,则直接从头顶处的邻居找答案。即Bag[2][3] = Bag[1][3]
,为60
如果一定要装入B物品,注意,此时为B物品背包腾出2
点空间后,剩余1
点承重。这1
点承重如果被最大化利用,取得了最高的价值,那么我们基于那个结果再装入B物品,就能取得“腾地方”这种方法的最大价值。
如何找到在仅有一点承重情况下背包可装的最大价值呢
?这个答案就存储在Bag[1][1]
中,即上一轮操作中背包承重为1
情况下背包可装的最大价值。(在下图中用蓝色椭圆圈出)
所以在“腾地方”这种方法下,Bag[2][3]
的值就是用Bag[1][1]
加上物品B的价值,即60+100
,得到160
最后,不装入B物品会获得60
的价值,腾完地方再装入B物品会获得160
的价值,所以最终将160
填入Bag[2][3]
。
讲到这里,相信你已经明白了解决0-1背包的基本思路:基于前面小问题的最好结果来进一步获得大问题的最好结果。
这也就是这一问题我们常听到的最优子结构性质。
现在我们接着完成第二行的表格。有了前面的经验,完成剩余内容应该不难。
要么从头顶照抄。要么先给B物品腾地方、找最优,然后再装。最后比谁更大。
计算过程和结果如下表所示:
在完成最后一行表格之前,我们来总结一下填写这个表格的规律:
如果进一步抽象,将物品的价值用v、重量用w这样的数组来表示,就有了下面这个最终的公式:
现在,来完成最后一行的Bag吧。
注意,当前物品为C,所以每次尝试为C “ 腾地方 ” 的时候,需要腾出3
点重量空间。
下表展示了部分计算过程:
“ 表格填完了,然后呢?”
还记得我们最开始的问题吗?
在背包承重上限为
5
的情况下,有ABC三个物品,每个物品各一个,怎样装能获得最大价值?求这个最大价值。
所以,现在需要在表格中,找考虑了ABC三个物品
,且背包最大承重为5
的那一格中存储的数字,就是题目要求的答案。即Bag[3][5]
中存储的220
。
我们填写整个Bag表格,最终的目的就是为了获得Bag[3][5]
这个位置的值。
Congratulations!你已经学会了解决0-1背包的方法。
一些扩展
【1】0-1背包的表格最完整的形式应该和下图中的一样。多添加了横竖为0的行列。尝试思考一下为什么要这么设计
【2】如果先考虑C、再考虑A、最后再考虑B,结果是一样的吗?
源码(C++):
#include <iostream>
#include<vector>
using namespace std;
int main()
{
int n, v; //n:物品数量,v:背包最大容量
cout << "请输入物品数量n 和背包容量v" << endl;
cin >> n >> v;
vector<int> w(n+1); //第n件物品的重量
vector<int> c(n+1); //第n件物品的价值
vector<vector <int>> bag;
for (int i = 0; i <= n; i++) { //初始化二维Bag表格,额外创建全0的第0行和第0列
vector<int> vec_row(v + 1);
bag.push_back(vec_row);
}
cout << "请按顺序输入物品价值c 和物品重量w" << endl;
for (int i = 1;i <= n;i++) //从1位置开始存储数据,0位置存储0
cin >> c[i] >> w[i];
for (int i = 1; i <= n;i++) { //从1位置开始遍历
for (int j = 1;j <= v;j++) { //从1位置开始遍历
if (w[i] > j) //如果当前物品重量大于当前背包承重,则无法放入,只能从“头顶”照抄
bag[i][j] = bag[i - 1][j];
else //如果可以装,就要判断是“头顶”照抄的价值大,还是“腾地方”找最优、最后装当前物品的价值大。
bag[i][j] = max(bag[i - 1][j], bag[i - 1][j - w[i]] + c[i]);
}
}
cout << "最大价值为" << bag[n][v] << endl;
return 0;
}
星砚完成于2024年3月26日
希望本篇的内容对你有所帮助。我们有缘再会!