1.1 题目
有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是 Ci,得到的价值是 Wi。求解将哪些物品装入背包可使价值总和最大。
1.2 基本思路
这是最基础的背包问题,只需要考虑选取哪几个物品放入背包,总体积不超过V的前提下价值最大。
特点是:每种物品仅有一件,可以选择放或不放。
若我们把取该物品记为1,不取该物品记为0,那么使用某种放入方式酱对应一个2进制串,因此这类问题也称为01背包问题。
用子问题定义状态:即 F[i, v] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。则其状态转移方程便是:
F[i, v] = max{F[i − 1, v], F[i − 1, V − Ci] + Wi}
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前 i 件物品放入容量为 V 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只和前 i − 1 件物品相关的问题。如果不放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入容量为 V的背包中”,价值为 F[i − 1, v];如果放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入剩下的容量为 V − Ci 的背包中”,此时能获得的最大价值就是 F[i − 1, V − Ci] 再加上通过放入第 i 件物品获得的价值 Wi。
伪代码如下:
F[0, 0..V ] ← 0
for i ← 1 to N
for v ← Ci to V
F[i, v] ← max{F[i − 1, v], F[i − 1, v − Ci] + Wi}
```
2.1例题
示例输入
1
5 10
1 2 3 4 5
5 4 3 2 1
样本输出
14
#include <queue>
#include <iostream>
#include <cstring>
#include <cmath>
#include <ctime>
#include <climits>
#include <vector>
#include <algorithm>
#include <map>
#include <cstdio>
#include <bits/stdc++.h>
#include <stack>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
int T,N,V;
int v[11000];//商品的体积
int w[11000];//商品的价值
int f[11000][11000];//动态规划表
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&N,&V);
memset(f,0,sizeof(f));
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 = V; j >= 0; j--){
if (j >= v[i]){//如果背包装得下当前的物体
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
else{//如果背包装不下当前物体
f[i][j] = f[i - 1][j];
}
}
}
cout<<f[N][V]<<endl;
}
return 0;
}
2.2 背包问题最优解回溯
通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:
- V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
- V(i,j)=V(i-1,j-v(i))+w(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-v(i));
- 一直遍历到i=1结束为止,所有解的组成都会找到。
就拿上面的例子来说吧:
- 最优解为V(5,10)=14,而V(5,10)!=V(4,10)却有V(5,10) = V ( 4 , 10 - v(5) ) + w(5) = V(4,9) + 5 = 9 + 5 =14,所以第5件商品被选中,并且回到V(4,10-v(5))=V(4,9);
- 有V(4,9)!=V(3,9)却有V(4,9) = V (3 , 10 - v(4) ) + w(4) =V(3,8) + 4 = 5 + 4 =9,所以第4件商品被选中,并且回到V(3,10-v(4))=V(3,8);
- 有V(3,8)!=V(2,8)却有V(3,8) = V (2 , 10 - v(3) ) + w(3) =V(2,7) + 3 = 2 + 3 =5,所以第3件商品被选中,并且回到V(2,10-v(3))=V(2,7);
- 有V(2,7)!=V(1,7)却有V(2,7) = V (1 , 10 - v(2) ) + w(2) =V(1,6) + 2 = 1 + 2 =3,所以第2件商品被选中,并且回到V1,10-v(2))=V(1,6);
- 有V(1,6)!=V(0,6)=0,且不存在V(1,6) = V (0 , 10 - v(1) ) + w(1) ,所以第1件商品没被选择。
2.3 代码实现
#include <queue>
#include <iostream>
#include <cstring>
#include <cmath>
#include <ctime>
#include <climits>
#include <vector>
#include <algorithm>
#include <map>
#include <cstdio>
#include <bits/stdc++.h>
#include <stack>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
int T,N,V;
int v[11000];//商品的体积
int w[11000];//商品的价值
int f[11000][11000];//动态规划表
int List[11000];//最优解情况
void Find(int i, int j) {//最优解情况
if (i >= 1) {
if (f[i][j] == f[i - 1][j]) {
List[i] = 0;
Find(i - 1, j);
}
else if (j - v[i] >= 0 && f[i][j] == f[i - 1][j - v[i]] + w[i]) {
List[i] = 1;
Find(i - 1, j - v[i]);
}
}
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&N,&V);
memset(f,0,sizeof(f));
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 = V; j >= 0; j--){
if (j >= v[i]){//如果背包装得下当前的物体
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
else{//如果背包装不下当前物体
f[i][j] = f[i - 1][j];
}
}
}
cout<<f[N][V]<<endl;
Find(N,V);
for (int i = 1; i <=N; i++) //最优解输出
cout << List[i] << ' ';
cout << endl;
}
return 0;
}
3.1优化
由于我们用f[n][V]表示最大价值,但是当物品和背包容量比较大时,这种用法会占用大量的空间,那么我们是不是对此可以进一步优化呢?
由上面我们可以知道,我们必须要做出n次选择,所以外层n次循环是必不可少的,对于上面代码的内层循环,表示当第i个商品要放入容量为V的背包时所获得的价值,即先对子问题求解,这个也是必不可少的,所以时间复杂度为O(nV),这个已不能进一步优化,但是我们可以对空间进行优化。
for (int i = 1; i <= N; i++){//动态规划
for (int j = V; j >= 0; j--){
if (j >= v[i]){//如果背包装得下当前的物体
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
else{//如果背包装不下当前物体
f[i][j] = f[i - 1][j];
}
}
}
可以发现如下问题:
(1)状态表f的遍历顺序为从第1行开始一行一行遍历,且在遍历第i行时候不会用到第i-2行数据,也就是i-2行及以前的数据没有用了,可以清除。同时,第i-1行的数据每个只会用到一次。
(2)遍历每一行时候只用到当前容量j和j-w[i]的数据,也就是第 i 次遍历只需要 第 i-1 次遍历中容量小于等于 j 的数据 。
现在考虑如果我们只用一位数组f[v]来表示f[i][v],是不是同样可以达到效果?
for (int i = 1; i <= n; i++)
{
for (int j = V; j >= v[i]; j--)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
从本质上说,这种优化方法针对了上述的三个问题:
(1)把遍历第i个物体和遍历第i-1个物体时的最大价值存在一个单元里。更新前f[j]存i-1的价值,更新后f[j]存i的价值。因为用不到i-2及以前的数据所以不需要存。因为以后不会再用到i-1的价值所以被覆盖了没问题。
(2)j从背包容量V开始遍历,即从大到小遍历,保证了当前f[j]和f[j - w[i]]里面存的是i-1的数据,即等价于f([i])[j] = max( f [i - 1] [j], f [i - 1] [ j - v[i] ] + w[i] ),从而和优化空间复杂度前状态转移方程的原理一致。
(3)自己给自己赋值,是无用操作,所以j < v[i]时候什么都不做即可。换句话说,只需要遍历到j >= v[i]。