Description
已知:有一个容量为W的背包和n件物品(同一种物品可多次选取),第i件物品的重量是w[i],价值是v[i]。
问题:在不超过背包容量的情况下,最多能获得多少价值或收益?
相似问题:在恰好装满背包的情况下,最多能获得多少价值或收益?
限制:在这里,每种物品可以挑选任意件.(与01背包问题的差别所在)
这里,我们先讨论在不超过背包容量的情况下,最多能获得多少价值或收益。
Think
问题特点:这次同一类的物品可以挑选任意多件了。
子问题定义状态:
dp[i][j]表示前i种物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值。
状态转移方程:
dp[i][j] = max(dp[i-1][j - k*w[i]] + k*v[i]) (0 <= k*c[i] <=j)
dp[i-1][j - k*w[i]] + k*v[i] :表示前i-1种物品中选取若干件物品放入剩余空间为 j-k*w[i]的背包中所能得到的最大价值 加上 k件第i种物品的价值
按照这个状态转移方程来写的话,需要三层循环:(时间复杂度:O(n*W*W))
Code(一)
for(int i=1; i<=n; i++)//对1—>n个物品进行枚举
{
for(int j=0; j<=W; j++)//对背包容量进行枚举
{
for(int k=0; k*w[i]<=j; k++)//对当前物品所拿的次数进行枚举
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*w[i]]+k*v[i]);
}
}
}
cout<< dp[n][W] <<endl;
由于这种时间复杂度太大了,所以我们可以在它的基础上进行优化。下面了解一下优化后的算法:
时间复杂度优化为O(nW)
将原始算法的DP思想转变一下。
设dp[i][j]表示出在前i种物品中选取若干件物品放入容量为j的背包所得的最大价值。
那么对于第i种物品的出现,我们对第i种物品放不放入背包进行决策。
<1>如果不放那么dp[i][j]=dp[i-1][j];
<2>如果确定放,背包中应该出现至少一件第i种物品,所以dp[i][j]种至少应该出现一件第i种物品,即dp[i][j]=dp[i][j-w[i]]+v[i]。为什么会是dp[i][j-w[i]]+v[i]?因为dp[i][j-w[i]]里面可能有第i种物品,也可能没有第i种物品。我们要确保dp[i][j]至少有一件第i件物品,所以要预留w[i]的空间来存放一件第i种物品。
状态转移方程:
dp[i][j] = max(dp[i-1][j] , dp[i][j-w[i]]+v[i])
Example
i(物品编号) | 1 | 2 | 3 |
w(物品重量) | 3 | 4 | 2 |
v(物品价值) | 4 | 5 | 3 |
可以看出ans = 10(物品1拿一个,物品三拿两个)
下面进行解释模拟一下各种状态之间是如何进行的(以加深自己它的理解):
行为i,列为j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 0 | 0 | 0 | 4 | 4 | 4 | 8 | 8 |
2 | 0 | 0 | 0 | 4 | 5 | 5 | 8 | 9 |
3 | 0 | 0 | 3 | 4 | 6 | 7 | 8 | 10 |
代码和01背包的计算有很大的相同点。在计算中感觉加深了对这个优化后的方程理解了!
Code(二)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int n = 3;//物品个数
const int W = 7;//背包最大容量
int w[n+1] = {0,3,4,2};//物品重量
int v[n+1] = {0,4,5,3};//物品价值
int dp[n+1][W+1];
int Knapsack()
{
memset(dp,0,sizeof(dp)); //初始化
//递推
for(int i=1; i<=n; i++) //枚举物品
{
for(int j=0; j<=W; j++) //枚举背包容量
{
dp[i][j] = dp[i - 1][j];
if (j >= w[i])
{
dp[i][j] = max(dp[i - 1][j],dp[i][j - w[i]] + v[i]);
}
}
}
// for(int i=1; i<=n; i++)
// {
// for(int j=0; j<=W; j++)
// {
// printf("%d ",dp[i][j]);
// }
// printf("\n");
// }
return dp[n][W];
}
int main()
{
cout<<Knapsack()<<endl;
return 0;
}
优化空间复杂度
其实完全背包也可以像01背包那样只开一个一维数组就完成运算的!(并且两者很相似)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int n = 3;//物品个数
const int W = 7;//背包最大容量
int w[n+1] = {0,3,4,2};//物品重量
int v[n+1] = {0,4,5,3};//物品价值
int dp[W+1];
int Knapsack()
{
memset(dp,0,sizeof(dp)); //初始化
//递推
for (int i = 1;i <= n;i++) //枚举物品
{
for (int j = w[i]; j<=W; j++) //枚举背包容量,下限为w[i],上限为W
{
dp[j] = max(dp[j],dp[j - w[i]] + v[i]);
}
}
return dp[W];
}
int main()
{
cout<<Knapsack()<<endl;
return 0;
}
两者不同在于,01背包是逆序枚举,完全背包是增序枚举
增序枚举背包容量会达到什么效果:它会重复的装入某个物品,而且尽可能多的,使价值最大,当然不会不超过背包容量
逆序枚举背包容量:背包中的物品至多装一次,使价值最大,当然不会不超过背包容量。
附上一篇博客:里面有详细的例子讲增序,和逆序。Click here