一.01背包问题
现在有N件物品和一个容量为V的背包。第i件物品的费用是cost[i],价值是value[i]。每个物品最多只能选一次,求解在不超过背包容量的限制下,如何选取物品组合能使收益最大化?
题型有两种,一种要求背包恰好放满,一种不要求背包恰好放满
二.思路
现在考虑第二种题型,即不要求背包恰好放满
将问题分解成子问题:有i件物品,容量为j的背包。
用dp[ i ][ j ]数组存储子问题的答案。
假设现在要求取dp[ i ] [ j ] ,我们考虑要不要选取第i件物品:
①如果不选取,则dp[ i ] [ j ]=dp[ i-1 ][ j ],因为dp数组储存的一直是当前状态下最优的解,dp[ i-1 ][ j ]存放的值是在同样容量为j的背包限制下前i-1件物品最大的价值组合
②如果选取i件物品,考虑第i件物品的花费cost[ i ],则dp[ i ][ j ]=dp[ i-1] [ j-cost[i] ] +value [ i ] , 而 dp[ i-1] [ j-cost[i] ] 存放的值是容量为j-cost[ i ]限制下, 前i-1件物品最大的价值组合
综上,得状态转移方程:
dp[ i ] [ j]=max(dp[ i-1 ][ j ], dp[ i-1] [j-cost[i] ] + value[ i ])
我们能保证不断得到每一个子问题的最优解,所以不断递推,即得到原问题的解
三.具体实现
由之前的思路可得,解每一个子问题dp[ i ][ j ]时,我们需要优先知道dp[ i-1 ][ j ]和dp[ i-1] [ j-cost[i] ] +value [ i ],观察可得必须先知道dp数组第i-1行的值
所有最外层循环让i从小到大递增
至于j是递增还是递减是无所谓的,因为求解子问题dp[ i ][ j ]时,我们只需要优先知道dp数组第i-1行的值,至于第i行的求解顺序是无所谓的
而考虑第一行,也就是只有一种物品时,那么j是从小到大还是从大到小都无所谓,因为第一行的dp值只取决于j是否大于cost[1]
后面几行都是第一行递推而来,第一行没问题后面几行自然没问题
int dp[10][2005];
memset(dp,0,sizeof(dp));
for(int i=1;i<8;i++)
for(int j=N;j>=1;j--){
if(j>=cost[i])
dp[i][j]=max(dp[i-1][j],dp[i-1][j-cost[i]]+value[i]);
else //如果这个物品装不下那就直接不装
dp[i][j]=dp[i-1][j];
}
至于之前说的第一种题型,只要把dp数组初始化为负无穷即可
负无穷表示没有方案使该子问题得解
四.空间优化(滚动数组)
由之前的分析可以发现,求dp数组第i行的值时,只需要用到dp数组第i-1行的值,而其它几行的值都是用不到的,所以为了节省空间,只需要用一维的dp数组即可
但是要注意这其实是在用一维数组来模拟二维数组的情况,想象一个不断滚动的数组
所以这里的第二层循环中的j必须倒序了,不像之前是无所谓的了
原因很简单,我们需要dp[ i-1] [ j-cost[ i ] ]的值,而如果j顺序地从小到大增加则该值已经被覆盖了,那么该行后面的元素在计算时使用的就不是上一行的dp[ i-1] [ j-cost[ i ] ] ,其实已经被覆盖为dp[ i] [ j-cost[ i ] ]
代码:
int dp[2005];
memset(dp,0,sizeof(dp));
for(int i=1;i<8;i++)
for(int j=N;j>=1;j--)
if(j>=cost[i])
dp[j]=max(dp[j],dp[j-cost[i]]+value[i]);
同时也不需要else dp[i][j]=dp[i-1][j];
这句代码了 ,因为不变相当于不去覆盖上一行的dp值