问题描述:
一位旅客(苦逼程序员)于山间探险,在一处地方发现了很多的宝石,然而旅客的背包容量有限,可能并不能将所有宝石全部带走,为了使自己得到的物品总价值最高,当场写下了一段代码,来计算如何拿取宝石使自己获利最高。
问:这段代码该如何实现?假设宝石体积与价值皆为整数,并且不准将宝石切开以一部分的形式带走。
题目剖析与解题思路:
在0-1背包问题当中,对于一颗宝石而言,其结果无非就是两种:【放/不放】,但是如果要细分,又可以分成:【放/不想放(价值不够高),不能放(容积不够)】
我们逐步分析各个情况:
- 放进背包:在背包容量足够,且放入这块宝石比不放入这块宝石价值高的时候,放入背包。
- 不想放进背包:在背包容量足够(可以拿出之前装入的宝石),但放入背包不如不放入背包时的价值高。
- 不能放进背包:无论宝石价值多高,因为背包容积不够导致不能放入背包。
对于程序而言,背包的容量j会随着循环的推进,逐步增加,而并不是一开始就假定背包拥有全部的容量,所以我之前所说的对于第二种情况的描述是有错误的,正确来说,任何情况下,只要背包容量 j >= 当前宝石体积,我们都会将宝石放入背包,只是当我们遇到这种情况时:
当前宝石体积 <= 背包剩余容量j
但此时背包内可能装了其他宝石,所以这里要用j - w[i],靠着dp的记载,查看当背包容量剩余j - w[i]时最大的宝石价值
此时的我们面临两个选择
选择1:无视该宝石,体现在代码里面为:dp[i][j] = dp[i - 1][j]
选择2:从当前位置直接往后找,查看是否可以丢弃当前宝石(至于丢弃了哪些宝石,我们是不知道的,dp数组巧妙地将宝石零散的价值,整合成了一组连续数据)的空间及其价值来装入当前宝石,体现在代码里面为:dp[i][j] = dp[i - 1][j - w[i]] + v[i]
将选择1和选择2进行比对,就会得出是否要抛弃背包中的一部分宝石而装入新的宝石了,体现在代码里面就是
if(w[i] <= j){
dp[i][j] = max(dp[i][j] = dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
这段代码代表的就是,对比过丢弃一些宝石装入当前宝石的价值和当前宝石的价值的比较
这里有几点需要格外注意,我们可以看到代码当中是用j - w[i],初次接触算法的同学可能会想怎么会有那么好的情况,刚好有一个宝石和当前宝石的体积一样,可以从背包拿出呢?这里就是一个误区,dp这个二维数组,存储的是,从[0,....,i]的宝石之间,选取宝石的体积为j,所以为dp[i][j]
举个例子,宝石体积为1,3,5,很明显,宝石的总体积只能为1、3、4、5、6、9
但这不代表我们不能知道背包容量j == 2时的数值,因为可以靠dp[i][j] = dp[i - 1][j]来获取
对于第三种情况而言,当宝石体积超过当前背包容积的时候,无法放入该宝石,因此不放入宝石
逐个解决各个情况,最后得出的就是答案。
这里依然使用前面的例子:【背包容积为3,宝石为3颗,重量分别为1,2,3,价值分别为1,2,10】。
需要说明的是,对于一颗宝石而言,无论我们选择放或者不放,这颗宝石都不再考虑,也即是说,当有n个宝石的时候,无论我们选择怎么处理其中一颗宝石A,n都必须减1,因为按照我们之前提出的三种情况而言。
情况1,代表我们放进宝石A,那么不再考虑这颗宝石了,无论我们之后会不会为了别的宝石而拿出宝石A。
情况2,代表宝石A不值得我们拿出之前的宝石而腾出空间放入,那么之后能拿到更高价值的宝石B时就更不用考虑宝石A了。
情况3,代表我们当前容积(就算这颗宝石的价值值得我们为它腾空背包也无法放入)无法放入宝石A,因此不考虑宝石A。
因此,在考虑了上述所说的情况后,我们可以总结出一个公式:1.放入背包,背包容积减去宝石体积,背包总价值加上宝石价值,不再考虑该宝石。2.不放入背包,背包容积不减去该宝石体积,背包总价值不变。
用一个二维数组来表示这个公式就是:
f[宝石数量][背包容积]=max{f[宝石数量-1][背包容积],f[宝石数量-1][背包容积-宝石体积]+宝石价值}
用n表示宝石数量,m表示背包容积,用w[]表示宝石体积,用v[]表示宝石价值,公式就可以写为。
f[n][m]=max{f[n-1][m],f[n-1][m-w[n]]+v[n]}。
这就是著名的0-1背包公式,这个公式极其重要,几乎在所有设计背包的问题上面都会运用到它。
这个公式正是在为我们比较放/不想放的情况下,我们该如何抉择。
那么,拿出之前的例子【背包容积为3,宝石为3颗,重量分别为1,2,3,价值分别为1,2,10】,如果总体来看,我们一颗颗来比较,虽然能解决问题,但会耗费大量的时间和空间,如果有n颗宝石,n无限大,那么一颗颗比较显然是不现实的,所以我们将问题拆解为一个个小问题。
假设我们的背包已经装好了n-3颗宝石的最优解,现在就只剩这三颗宝石和这些容积了,这三颗宝石的最优解必然是总体最优解的一部分,因为动态规划的一个特点便是
最优子结构性质:局部最优解是整体最优解的一部分,这个时候我们会想,既然都已经分成三个宝石了,那么能不能再分一次?分成2颗甚至1颗宝石?
于是我们将三颗宝石再次划分,分为三颗独立的宝石,我们逐步分析,当只有第一颗宝石的时候,背包容积为3,符合我们之前所说的第一种情况,显然装入它的时候是目前的最优解,不装入什么都得不到,而装入宝石,背包价值就会+1。
当拥有第一第二颗宝石的时候,依然符合我们之前所说的第一种情况,再次装入该宝石,背包价值+2,总价值为3。
当拥有第三课宝石的时候就需要进行比较和抉择了,此时背包已经装入第一和第二颗宝石,容积为0,是否需要舍弃之前拿进来的两颗宝石然后装入第三颗宝石呢?如果要装入,我们要如何舍弃之前的宝石呢?是舍弃其中一颗?还是全部舍弃?这里,我们就可以得知,对背包容积的控制,其实就是在控制宝石的舍取。假设背包已经装满了,容积为0,我们控制背包容积越大,代表舍弃的宝石越多,当把宝石全部舍弃都无法装入目标宝石的时候,代表第三种情况,所以只能舍弃目标宝石。返回例子,我们的背包现在装入了两颗宝石,容积为0,我们舍弃第一颗宝石的情况下,容积+1,无法放入第三课宝石,我们继续舍弃,舍弃第二颗宝石,容积+2,依然无法放入,我们将两颗宝石全部舍弃,容积+3,可以放入了,于是我们就比较只放入第三颗宝石和放入第一和第二颗宝石那个价值高,提取最大值,最后的答案就是我们所要的结果。
代码实现:
以下为C++代码的实现,i和j控制宝石数量和背包容积
#include <iostream>
using namespace std;
int Max(int a,int b)
{
int max=0;
if(a>=b)
{
max=a;
}
else
{
max=b;
}
return max;
}
int knapsack(int item_value[],int item_volume[],int Max_value[100][100],int item_number,int Bag_volume)
{
int i=1;
int j=1;
for(i=1;i<=item_number;i++) //这里的i控制的是第几个宝石,我们逐个对宝石进行分析
{
for(j=1;j<=Bag_volume;j++) //这里的j控制背包容积,从背包容积为1开始逐个增加,在增加过程中就可以看出抛弃了什么宝石
{
if(j>=item_volume[i])
{
Max_value[i][j]=Max(Max_value[i-1][j],Max_value[i-1][j-item_volume[i]]+item_value[i]);
}
else
{
Max_value[i][j]=Max_value[i-1][j];
}
}
}
return 0;
}
int main()
{
int i=1;
int j=1;
int item_number;
int Bag_volume;
int Max_value[100][100];
cout<<"请输入背包体积:";
cin>>Bag_volume;
cout<<"请输入物品个数:";
cin>>item_number;
int *item_value=new int[item_number+1];
int *item_volume=new int[item_number+1];
cout<<"请输入物品重量"<<endl;
for(i=1;i<=item_number;i++)
{
cin>>item_volume[i];
}
cout<<"请输入物品价值"<<endl;
for(i=1;i<=item_number;i++)
{
cin>>item_value[i];
}
for(i=0;i<=item_number;i++)
{
for(j=0;j<=Bag_volume;j++)
{
Max_value[i][j]=0;
}
}
knapsack(item_value,item_volume,Max_value,item_number,Bag_volume);
cout<<"最大价值为:"<<Max_value[item_number][Bag_volume]<<endl;
cout<<endl;
return 0;
}
//这里的j控制背包容积,从背包容积为1开始逐个增加,在增加过程中就可以看出抛弃了什么宝石
{
if(j>=item_volume[i])
{
Max_value[i][j]=Max(Max_value[i-1][j],Max_value[i-1][j-item_volume[i]]+item_value[i]);
}
else
{
Max_value[i][j]=Max_value[i-1][j];
}
}
}
return 0;
}
int main()
{
int i=1;
int j=1;
int item_number;
int Bag_volume;
int Max_value[100][100];
cout<<"请输入背包体积:";
cin>>Bag_volume;
cout<<"请输入物品个数:";
cin>>item_number;
int *item_value=new int[item_number+1];
int *item_volume=new int[item_number+1];
cout<<"请输入物品重量"<<endl;
for(i=1;i<=item_number;i++)
{
cin>>item_volume[i];
}
cout<<"请输入物品价值"<<endl;
for(i=1;i<=item_number;i++)
{
cin>>item_value[i];
}
for(i=0;i<=item_number;i++)
{
for(j=0;j<=Bag_volume;j++)
{
Max_value[i][j]=0;
}
}
knapsack(item_value,item_volume,Max_value,item_number,Bag_volume);
cout<<"最大价值为:"<<Max_value[item_number][Bag_volume]<<endl;
cout<<endl;
return 0;
}
思考问题:
虽然我们能知道最后的最大价值,但并不能得出我们拿走了哪些宝石,所以并没有完全实现0-1背包,因为在0-1背包中,0代表不拿该宝石,1代表拿取该宝石,所以这里依然要思考如何实现路径的寻找与存储,希望能和有兴趣的朋友一起讨论。