先读题目
现有一最多承重量为10Kg的背包和以下物品,且物品都不可拆分:
物品 | 重量 | 价值 |
1 | 2 | 20 |
2 | 1 | 12 |
3 | 6 | 8 |
4 | 5 | 32 |
问:背包所能装下物品的最大价值是多少?
寻找规律
初次接触0-1背包问题时会发现贪心算法并不能解决此类问题,并且隐藏在其中的递推关系也不那么好发现,可能脑子里只能想到用暴力破解的方法枚举所有可能从中找到最优解。
其实在0-1背包问题中是存在最优子结构和重叠子问题的,现在可以不用去过多想这两个名词,只用知道0-1背包中是存在递推关系的,为了方便理解这种递推关系将用更加形象的填表格方式进行解释,耐心看到最后思路就会清晰。
第一步:
从左到右从上到下填充表格第一块,当考虑前1个物品并且背包承重为1时,1号物品重量为2,2明显大于当前背包承重,故背包装不下该物品,此时背包价值为0。
背包当前承重→ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
考虑前n个物品↓ | ||||||||||
1 | 0 | |||||||||
2 | ||||||||||
3 | ||||||||||
4 |
第二步:
填充表格第二块,这时背包承重为2,前1个物品中物品1的重量正好也是2,2等于当前背包承重,故背包可以装下该物品,此时背包价值为20,在这一行剩下的空中背包承重继续递增至10也只考虑前1个物品时背包所能装下物品的最大价值,所以再怎么增加背包承重,其最大价值也都是20。
背包当前承重→ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
考虑前n个物品↓ | ||||||||||
1 | 0 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
2 | ||||||||||
3 | ||||||||||
4 |
第三步:
填充表格第二行,从第二行开始情况会变得复杂起来,我们先看第一个格子:当前所考虑的2号物品重量为1,当前背包承重为1,背包刚好可以装下该物品,如果装上该物品这时背包的价值是12;若不装2号物品这时背包的价值是0,这和考虑前1个物品并且背包承重为1时的情况是相同的。一种情况背包所装物品价值是12,另一种情况背包所装物品价值是0,通过比较当前最优解显然是12。我们选择装2号物品。
背包当前承重→ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
考虑前n个物品↓ | ||||||||||
1 | 0 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
2 | 12 | |||||||||
3 | ||||||||||
4 |
第四步:
填充第二行第二个格子 ,我们同样这样想:当背包容量为2时,现在考虑前2个物品,对于当前所考虑的1号物品其重量为2可以被背包装下;2号物品重量为1也可以被装下。这时问题貌似变得更为复杂,好像脑子转不过来一样,越往后面填越乱以至于又回到了最初的暴力枚举模式。其实现在应该冷静下来先分析一下:
其实不管情况再怎么复杂对于新加入考虑范畴的2号物品无非只有两种情况:装和不装!
如果我们不装2号物品,那么现在的最优解就是考虑前1个物品时的最优解,这个解早在填写第一行时已经得出,就是20。
如果我们装2号物品,2号物品的体积为1,那么装下2号物品后背包的容量为2-1=1,背包的价值变为12,这时对于背包剩下的容量该如何考虑呢?其实我们在填写第一行时也已经给出了答案:这时2号物品已经装下,我们要考虑的是前1个物品的情况,并且装下2号物品背包还剩1点容量。这不就是背包容量为1并且考虑前1个物品时背包所能装下最大价值是多少的问题吗?所以这时的最优解的构成就是2号物品的价值12加上背包承重为1时考虑前1个物品的最优解0,为12+0=12。
考虑完2号物品装与不装的所有情况也不要忘了题目要求,是要求出当前状态的最优解,只需把装2号物品和不装2号物品进行比较即可:12<20。显然选择不装2号物品。
看到到这一步对不装当前新加入考虑范畴的物品应该可以理解透彻了,就是和不加入该物品时的情况相同
若装下当前新加入考虑范畴的物品,那么背包必须要分给和该物品重量等值的承重量,背包在减去这部分承重量之后再次考虑背包剩余的这些承重还能装下物品(这时考虑的物品不包含已经装下的物品,即要考虑的是装下物品之前的各物品)的最大价值,把这两部分相加就是装入该物品时的最优解。
如果看到这里对与于装该物品可能还不能完理解,可以继续往下看第五步
背包当前承重→ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
考虑前n个物品↓ | ||||||||||
1 | 0 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
2 | 12 | 20 | ||||||||
3 | ||||||||||
4 |
第五步
到了第五步情况变得明了起来,此时背包容量为3,物品1体积为2;物品2体积为1,这时考虑前两个物品背包能完全装下所有考虑范畴之内的物品,这时显然的,我们也可以用第四步中给出的思路再次分析一下:
对于2号物品只有装和不装两种可能
若不装2号物品,考虑前1个物品时,背包承重量为3时情况,最优解为20
若装2号物品,2号物品的重量为1,在给2号物品分配足够的承重量后背包还剩下3-1=2点承重量,这时背包已经容纳2号物品,并且还剩余的2点承重量,我们在考虑剩下的承重量还能装下物品的最大价值时自然要排除2号物品变为考虑前2-1个物品,也就是前1个物品,这种情况我们在填写第一行时也早已得出,查表可以得出:背包承重量为2,考虑前1个物品背包所装物品的最大价值为20。所以在装下2号物品时背包的价值为12+20=32。
将装2号物品和不装2号物品两种情况背包所装物品价值进行比较:32>20。显然选择装下2号物品。
之后的表格也如此考虑可填完第二行。
背包当前承重→ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
考虑前n个物品↓ | ||||||||||
1 | 0 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
2 | 12 | 20 | 32 | 32 | 32 | 32 | 32 | 32 | 32 | 32 |
3 | ||||||||||
4 |
第六步
可以试着按这个方法填完所有表格:
背包当前承重→ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
考虑前n个物品↓ | ||||||||||
1 | 0 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
2 | 12 | 20 | 32 | 32 | 32 | 32 | 32 | 32 | 32 | 32 |
3 | 12 | 20 | 32 | 32 | 32 | 32 | 32 | 32 | 40 | 40 |
4 | 12 | 20 | 32 | 32 | 32 | 44 | 52 | 64 | 64 | 64 |
此时想必已经发现了最终的规律:
设动态规划数组dp[i][j],其含义是在考虑前i个物品且背包容量为j时背包所容纳物品的最大价值
若背包不能装下第i个物品:
背包最大价值和dp[i-1][j]时情况相同
若背包能装下第i个物品分两种情况:
装第i个物品时最大价值为dp[i-1][j-weight[i]] + value[i]
不装第i个物品时最大价值为dp[i-1][j]
用max函数进行比较取出两者最大值max(dp[i-1][j-weight[i]] + value[i], dp[i-1][j])
确定递推公式
根据上面的描述,最终的递推公式会是这样:
if (j < weight[i-1]){
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = max(dp[i-1][j-weight[i-1]] + value[i-1], dp[i-1][j]);
//这里由于数组下标是从0开始,要做减去1的处理
}
dp数组遍历顺序
在上面填表的过程中遍历顺序已经清晰明了:先遍历物品,再遍历背包。
其实大家可以尝试一下先遍历背包,后遍历物品,也可以成功递推出整个表格。
所以此题先遍历背包或先遍历物品都是可以的,这里只以先遍历物品为例,所以在程序中写出的for循环应该是这样的:
for (i = 2; i <= weight.size(); i++){
for (j = 2; j <= bag; j++){
//此处填写递推公式
}
}
dp数组的初始化
对于dp数组的初始化问题只需要关注递推公式中给出的递推关系即可,可以这样考虑:
递推公式中的dp[i][j]由dp[i-1][j]或dp[i-1][j-weight[i-1]]推导出在上面表格中i和j的最小值都是1,不能再做i-1这样的操作,所以i只能从2开始遍历,1所对应的情况就要进行初始化,j也如同这样考虑可以得出:有了第一行和第一列的1数据,整个表格的数据都能由他们得出,dp数组初始化时初始化第一行和第一列即可,初始化的方法和上面填表的方法相同。
//初始化第一列
for (i = 1; i <= weight.size(); i++){
if (weight[i-1] <= 1){
dp[i][1] = value[i-1];
}
else{
dp[i][1] = dp[i-1][1];
}
}
//初始化第一行
for (j = 1; j <= bag; j++){
if (weight[0] <= j){
dp[1][j] = value[0];
}
else{
dp[1][j] = 0;
}
}
完整代码展示
#include<iostream>
#include<vector>
using namespace std;
void solving_01_knapsack_problem(){
int bag = 10;
int i,j;
vector<int> weight = {2, 1, 6, 5}; //物品重量
vector<int> value = {20, 12, 8, 32};//物品价值
vector<vector<int>> dp(weight.size() + 1, vector<int>(bag + 1, 0));
//初始化第一列
for (i = 1; i <= weight.size(); i++){
if (weight[i-1] <= 1){
dp[i][1] = value[i-1];
}
else{
dp[i][1] = dp[i-1][1];
}
}
//初始化第一行
for (j = 1; j <= bag; j++){
if (weight[0] <= j){
dp[1][j] = value[0];
}
else{
dp[1][j] = 0;
}
}
//进行递推
for (i = 2; i <= weight.size(); i++){
for (j = 2; j<=bag; j++){
if (j < weight[i-1]){
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = max(dp[i-1][j-weight[i-1]] + value[i-1], dp[i-1][j]);
}
}
}
//打印dp数组
cout << "dp数组如下:" << endl << endl;
for (i = 1; i <= weight.size(); i++){
for (j = 1; j <= bag; j++){
printf ("%5d", dp[i][j]);
}
cout << endl << endl;
}
//打印最终结果
cout << "该背包所能容纳物品的最大价值为:" << dp[i-1][j-1] << endl << endl;
}
int main(){
solving_01_knapsack_problem();
}