参考《背包九讲》
一、题目
有 N 件物品和一个容量为 V 的背包
放入第 i 件物品耗费的空间是 Ci,得到的价值是 Wi
求解将哪些物品装入背包可使价值总和最大
二、基本思路
特点:
每种物品仅有一件,可以选择放或不放
用子问题定义状态:
dp[i, V] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值
则其状态转移方程便是:
dp[i, V] = max{dp[i − 1, V], dp[i − 1, V − Ci] + Wi}
那么就可以转化为一个只和前 i − 1 件物品相关的问题
如果不放第 i 件物品,那么问题就转化为“ 前 i − 1 件物品放入容量为 V 的背包中 ”,价值为 dp[i − 1, V]
如果放第i件物品,那么问题就转化为“前 i − 1 件物品放入剩下的容量为 V − Ci 的背包中”
此时能获得的最大价值就是 dp[i − 1, V − C i] 再加上通过放入第 i 件物品获得的价值 W i
POJ 3624 Charm Bracelet
//时间和空间复杂度均为O(V * N)
//Result:MLE
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 3410;
int W[maxn];
int D[maxn];
int dp[maxn][12890];
int main()
{
// freopen("in.txt", "r", stdin);
int N, M;
scanf("%d%d", &N, &M);
for (int i=1; i<=N; ++i) {
scanf("%d%d", &W[i], &D[i]);
}
memset(dp, 0, sizeof(dp));
for (int i=1; i<=N; ++i) {
for (int j=1; j<W[i]; ++j) {
dp[i][j] = dp[i-1][j];
}
for (int j=W[i]; j<=M; ++j) {
dp[i][j] = max(dp[i-1][j-W[i]] + D[i], dp[i-1][j]);
}
}
printf("%d\n", dp[N][M]);
return 0;
}
三、优化空间复杂度
以上方法的时间和空间复杂度均为O(V * N),其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到O(V)
上面的基本思路:有一个主循环 i = 1..N,每次算出来二维数组 dp[i, 0..V ] 的所有值
那么,如果只用一个数组 dp[0..V ],能不能保证第 i 次循环结束后 dp[V] 中表示的就是我们定义的状态 dp[i, V] 呢?
dp[i, v] 是由 dp[i − 1, v] 和 dp[i − 1, v − Ci] 两个子问题递推而来
能否保证在推 dp[i, v] 时(也即在第 i 次主循环中推 dp[v] 时)能够取用 dp[i − 1, v] 和 dp[i − 1, v − Ci] 的值呢?
事实上,这要求在每次主循环中我们以 v = V..0 的递减顺序计算dp[v]
这样才能保证推 dp[v]时 dp[v − Ci] 保存的是状态 dp[i − 1,v − Ci] 的值
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 3410;
int W[maxn];
int D[maxn];
int dp[12890];
int main()
{
// freopen("in.txt", "r", stdin);
int N, M;
scanf("%d%d", &N, &M);
for (int i=1; i<=N; ++i) {
scanf("%d%d", &W[i], &D[i]);
}
memset(dp, 0, sizeof(dp));
for (int i=1; i<=N; ++i) {
for (int j=M; j>=W[i]; --j) {
if (dp[j-W[i]] + D[i] > dp[j]) {
dp[j] = dp[j-W[i]] + D[i];
}
}
}
printf("%d\n", dp[M]);
return 0;
}
四、初始化的细节问题
求最优解的背包问题题目中,事实上有两种不太相同的问法
有的题目要求 “恰好装满背包” 时的最优解
有的题目则并没有要求必须把背包装满
一种区别这两种问法的实现方法是在初始化的时候有所不同
如果是第一种问法,要求恰好装满背包
那么在初始化时除了 dp[0] 为 0,其它 dp[1..V ] 均设为 −∞,这样就可以保证最终得到的 dp[V] 是一种恰好装满背包的最优解
如果并没有要求必须把背包装满,而是只希望价格尽量大
初始化时应该将 dp[0..V ] 全部设为 0
这是为什么呢?可以这样理解:
初始化的F 数组事实上就是在没有任何物品可以放入背包时的合法状态
如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且价值为 0 的情况下被“恰好装满”
其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 -∞ 了
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”
这个解的价值为 0,所以初始时状态的值也就全部为 0 了
五、小结
01背包问题是最基本的背包问题
它包含了背包问题中设计状态、方程的最基本思想
另外,别的类型的背包问题往往也可以转换成01背包问题求解
故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及空间复杂度怎样被优化
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long LL;
const int maxn = 100 + 10;
double Pj[maxn];
int Mj[maxn];
double dp[10010];
int main()
{
#ifdef __AiR_H
freopen("in.txt", "r", stdin);
#endif // __AiR_H
int T;
while (scanf("%d", &T) != EOF) {
while (T--) {
double P;
int N;
int sum = 0;
scanf("%lf%d", &P, &N);
for (int i = 1; i <= N; ++i) {
scanf("%d%lf", &Mj[i], &Pj[i]);
sum += Mj[i];
}
memset(dp, 0, sizeof(dp));
dp[0] = 1;
for (int i = 1; i <= N; ++i) {
for (int j = sum; j >= Mj[i]; --j) {
dp[j] = max(dp[j], dp[j - Mj[i]] * (1 - Pj[i]));
}
}
for (int i = sum; i >= 0; --i) {
if (dp[i] >= (1-P)){
printf("%d\n", i);
break;
}
}
}
}
return 0;
}
解题思路:
最后买最贵的菜(可能有多个)
然后就是求 V = m-5 且不考虑第一个最贵饭菜的01背包问题
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1000 + 10;
int price[maxn];
int dp[1010];
int main()
{
#ifdef __AiR_H
freopen("in.txt", "r", stdin);
#endif // __AiR_H
int n;
while (scanf("%d", &n) != EOF && n != 0) {
int Max;
int t;
for (int i = 1; i <= n; ++i) {
scanf("%d", &price[i]);
if (i == 1) {
Max = price[i];
t = 1;
} else if (price[i] > Max) {
Max = price[i];
t = i;
}
}
int m;
scanf("%d", &m);
if (m < 5) {
printf("%d\n", m);
} else {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; ++i) {
if (i != t) {
for (int j = m-5; j >= price[i]; --j) {
dp[j] = max(dp[j], dp[j-price[i]] + price[i]);
}
}
}
printf("%d\n", m-dp[m-5]-Max);
}
}
return 0;
}
HDU 1171 Big Event in HDU
转化为 V = halves 的01背包问题
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 50 + 10;
int V[maxn];
int M[maxn];
int dp[130000];
int main()
{
#ifdef __AiR_H
freopen("in.txt", "r", stdin);
#endif // __AiR_H
int N;
while (scanf("%d", &N) != EOF && N >= 0) {
int sum = 0;
for (int i = 1; i <= N; ++i) {
scanf("%d%d", &V[i], &M[i]);
sum += (V[i] * M[i]);
}
int halves = sum / 2;
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= N; ++i) {
for (int j = 0; j < M[i]; ++j) {
for (int k = halves; k >= V[i]; --k) {
dp[k] = max(dp[k], dp[k-V[i]] + V[i]);
}
}
}
printf("%d %d\n", sum-dp[halves], dp[halves]);
}
return 0;
}