一 背包问题
(1)01 背包 :
给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
分析:对于每个物品,我们都有两种选择,取和不取。
我们可以定义一个二维数组dp[i][j],表示有i件物品,背包容量为j时获得的最大价值。
对于dp[i][j],当w[i]>j时,dp[i][j] = dp[i-1][j];否者,dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);再放与不放中选着价值 最大的。得状态转移方程:
if (背包体积j小于物品i的体积) f[i][j] = f[i-1][j] //背包装不下第i个物体,目前只能靠前i-1个物体装包 else f[i][j] = max(f[i-1][j], f[i-1][j-Wi] + Vi)
我们可以把这个过程看成填一个表
例如:
价值数组v = {8, 10, 6, 3, 7, 2},
重量数组w = {4, 6, 2, 2, 5, 1},
背包容量C = 12时对应的dp[i][j]数组。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
1 | 0 | 0 | 0 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
2 | 0 | 0 | 0 | 8 | 8 | 10 | 10 | 10 | 10 | 18 | 18 | 18 |
3 | 0 | 6 | 6 | 8 | 8 | 14 | 14 | 16 | 16 | 18 | 18 | 24 |
4 | 0 | 6 | 6 | 9 | 9 | 14 | 14 | 17 | 17 | 19 | 19 | 24 |
5 | 0 | 6 | 6 | 9 | 9 | 14 | 14 | 17 | 17 | 19 | 21 | 24 |
6 | 2 | 6 | 8 | 9 | 11 | 14 | 16 | 17 | 19 | 19 | 21 | 24 |
如m[2][6],在面对第二件物品,背包容量为6时我们可以选择不拿,那么获得价值仅为第一件物品的价值8,如果拿,就要把第一件物品拿出来,放第二件物品,价值10,那我们当然是选择拿。m[2][6]=m[1][0]+10=0+10=10;依次类推,得到m[6][12]就是考虑所有物品,背包容量为C时的最大价值。
伪代码:
int w[maxn] = {0,1,2,3,4,5...}; int v[maxn] = {0,5,4,3,2,1...}; memset(dp,0,sizeof(dp)); for(int i = 1 ; i <= n ;i ++ ){ for(int j = 1 ; j < = sum_w ; j ++ ){ if(j<w[i]) dp[i][j] = dp[i-1][j]; else dp[i][j] = max(dp[i-1][j],dp[i-1][dp[i-1][j-w[i]]]+v[i]); } } cout<<dp[n][sum_w]<<endl;
当数据量大时,这种二维数组可能就不适用了,我们可以采用滚动数组的方法。
先给出代码:
int w[maxn] = {0,1,2,3,4,5...}; int v[maxn] = {0,5,4,3,2,1...}; memset(dp,0,sizeof(dp)); for (int i = 1;i <= n;i++) { for (int j = sum_w;j >= wi;j--) { dp[j] = max(dp[j],dp[j - w[i]] + v[i]); } } cout<<dp[sum_w]<<endl;
代码分析:
dp数组是从上到下,从右到左计算的,再计算(i,j)的时候,dp[j]里保存的就是dp(i-1,j)的值,而dp[j-w]里保存的是
dp(i-1,j-w)而不是dp(i,j-w)——因为dp是逆序枚举的,此时的dp(i,j-w)还没有算出来。这样,
dp[j] = max(dp[j],dp[j - w[i]] + v[i])实际上是把max(dp(i-1,j),dp(i-1,j-w))保存在dp[j]中,覆盖掉dp[j]原来的dp(i-1,j);
(2)完全背包
有N种物品和一个容量为V的背包。第i种物品有若干件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
分析:这个问题是01背包的升级版,不同的地方是一种物品可以放很多件,所以在01背包两重循环的前提下,在增加一重循环用来表示取的件数
状态转移方程:
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
伪代码:
int w[maxn] = {0,1,2,3,4,5...}; int v[maxn] = {0,5,4,3,2,1...}; memset(dp,0,sizeof(dp)); for(int i = 1 ; i <= n ;i ++ ){ for(int j = 1 ; j < = sum_w ; j ++ ){ if(c[i]<=j) for(int k = 0 ; k*w[i]<=j ;k ++ ) dp[i] = max(dp[i-1][j],dp[i-1][j-k*w[i]]+k*v[i]); else dp[i] = dp[i-1][j]; } } cout<<dp[n][sum_w]<<endl;
对于完全背包的滚动数组写法,影响dp[j]的是当前i种,而不是前i-1种,所以需要正序
int w[maxn] = {0,1,2,3,4,5...}; int v[maxn] = {0,5,4,3,2,1...}; memset(dp,0,sizeof(dp)); for (int i = 1;i <= n;i++) { for (int j = w[i];j <= sum_w ; j++) { dp[j] = max(dp[j],dp[j - w[i]] + v[i]); } } cout<<dp[sum_w]<<endl;
多重背包
分析:多重背包可以成01背包和完全背包的结合,我们可以将相同的物品看不不同的物品,然后看作是01背包进行求解
代码:
for(int i=1; i<=n; i++)//每种物品 for(int k=0; k<num[i]; k++)//其实就是把这类物品展开,调用num[i]次01背包代码 for(int j=m; j>=weight[i]; j--)//正常的01背包代码 dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
这其实和完全背包的代码一样。。。。真神奇。。
over!