背包问题是动态规划的经典问题,是指在一个有容积或者重量限制的条件下放入物品,物品有重量,价值还有编号或者其他属性,要求不超过容量的前提下,使得背包内的物品价值最大,常见的背包类型有01背包,完全背包,分组背包,多重背包等。
01背包:一共有n种物品,每种物品都有不同的重量和价值,每种物品只有一个,要不不放(0),要么放入(1),所谓01背包
解题思路:
1.每一种物品都有放或者不放的选择,随着背包容量的增多,存放的物品种类增加,每一个物品的增加都是在之前已经选择最大价值物品基础上的延伸,最优解包含最优子结构,无后效性体现在选择到当前物品,不去考虑后面的物品的价值,只关心在现有背包容量的基础上和现有的物品中选择最大值
2.设置状态:利用dp【i】【j】来表示前i个物品在容量为j的背包中的最大价值
3.状态转移:当面临第i种物品时,如果不放入背包(j<w[i])的话,那么此时的价值还是前i-1种商品的价值,即dp[i][j]=dp[i-1][j];当放入第i种物品时(j>=w[i]),那么必须意味着牺牲背包的空间,前i种商品的价值为dp[i-1][j-w[i]],因为此时第i种物品要占据w[i]的容量,然后再加上第i件商品的价值,所以状态转移方程为dp[i][j]=dp[i-1][j-w[i]]+v[i]
4.第三步如果放入背包后就一定是价值增多了么?不是,因为如果你放入这个物品,背包容量会减少,前i种商品可能会因为背包不足而扔掉,如果扔掉的商品的价值比放入的商品价值大的话,那么还不如不放,所以状态转移方程为dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]),选择较大值
5.初始化,当背包容量为0的时候,有再多的商品也放不进来,所以价值为0,当商品数为0的时候,背包再大也同样没有价值,即dp[i][0]=0,dp[0][j]=0;
6.根据递推公式填表,求目标值d[n][w],n为商品数量,w为背包容量
#include<bits/stdc++.h>
using namespace std;
int dp[1010][1010],w[1010],v[1010];
int main()
{
int n,W;//表示物品数量和背包容量
cin>>n>>W;
for(int i=1;i<=n;i++)
cin>>w[i];//输入每件物品的重量
for(int i=1;i<=n;i++)
cin>>v[i];//输入每件物品的价值
for(int i=1;i<=n;i++)//枚举每件商品
{
for(int j=1;j<=W;j++)//枚举背包容量
{
if(j<w[i])//如果放不下第i件商品
dp[i][j]=dp[i-1][j];//继承前i-1件商品的价值
else//如果可以放下
dp[i][j]=max(dp[i-1][j-w[i]]+v[i],dp[i-1][j]);//选择较大值
}
}
cout<<dp[n][W];//输出目标值
return 0;
}
拓展延伸:如何输出是哪些商品放入背包了呢?
1.由上述表格可以清楚的看到,如果将第i件物品没有放入背包,是继承i-1的最大价值,即此时的值和上一行同列的值是相等的,如果放入背包后,那么此时的价值是上一行第j-w[i]列的值+此时商品的值。
2.所以可以根据求得的目标值倒推回去,如果目标值和上一行相等,则该件商品,没有放入背包,打上标记,如果价值不相等,则该件商品放入了背包,何时放进去的呢?就是在上一行第j-w[i]列放入的,然后依次判断,直到i==0
3.根据标记数组输出放入背包商品的编号
代码实现
int temp=W;//设置书签 bool flag[n+1];//设置标记数组 for(int i=n;i>=1;i--)//从最后一行倒推 { if(dp[i][temp]==dp[i-1][temp])//如果此时价值和上一行价值相等 flag[i]=0;//说明该件商品没有放入 else//否则的话 { flag[i]=1;//背包中有这件商品 temp=temp-w[i];//书签跳转到第temp-w[i]列,此时加入的背包 } } for(int i=1;i<=n;i++) if(flag[i]==1) cout<<i<<" ";
空间优化:
1.在上述的填表过程中,时间和空间复杂度都是n*W的,如果遇到数据量大的时候,二维数组的大小受到空间限制,那么如何压缩空间呢?
2.在枚举每一个商品的时候,当背包容量放不下该商品的时候,都是直接继承上一行即dp[i-1][j],继承的时候是背包价值是不更新的,对于新加进来的物品,直接从j=w[i]来判断即可,因为此时的背包容量恰好能放下第i件商品,从此列到以后的值,才是真正做选择和决策的地方
3.所以利用一个一维数组来维护最大价值,每次从第W列倒推到W-w[i]列即可,为什么是倒推?因为一维数组是实时变化更新的值,为了不产生旧值还没利用完就被新值覆盖的情况,所以倒着推
4.那么状态转移方程也就变成了dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
5.输出dp【W】即为目标值,空间复杂度为O(W)
#include<bits/stdc++.h>
using namespace std;
int w[100010],v[100010],dp[100010];
int main()
{
int n,W;
cin>>n>>W;
for(int i=1;i<=n;i++)
cin>>w[i];
for(int i=1;i<=n;i++)
cin>>v[i];
for(int i=1;i<=n;i++)//枚举每一个物品
{
for(int j=W;j>=w[i];j--)//倒着推,直到恰好放下他的背包容量
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);//选择较大值
}
}
cout<<dp[W];
return 0;
}