有N件物品,一个容量为V的背包。放入第i件物品耗费的空间是Ci,得到的价值是Wi。
为了便于阅读,本文把F[i][v]写为F[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] + Wi
例如F[2, v] = MAX(F[1, v], F[1, v - C[2]] + W[2])
前2件物品放入容量为v的背包中,如果放第2件物品,问题就转化为“第1件物品放入容量为v的背包中”;如果不放第2件物品,问题就转化为“第1件物品放入容量为v - C[2]的背包中”。其中v要枚举至V,因为我们不能确定v的值。
代码如下:
/*
01背包问题,时间空间复杂度都为O(V*N)
F[i, v] = max(F[i - 1, v], F[i - 1, v - Ci] + Wi)
*/
#include <iostream>
using namespace std;
const int num = 100;
int main()
{
//------输入------
int N, V; //有N件物品,一个容量为V的背包
cin >>N >>V;
int C[num],W[num]; //放入第i件物品耗费的空间是C[i],得到的价值是W[i]。
for(int i = 1; i <= N; i++)
cin >>C[i] >>W[i];
int F[num][num * 10];
memset(F, 0, sizeof(F));
//------处理------
for(int i = 1; i <= N; i++)
for(int v = C[i]; v <= V; v++)
F[i][v] = F[i - 1][v] > F[i - 1][v - C[i]] + W[i] ? F[i - 1][v] : F[i - 1][v - C[i]] + W[i];
//------输出------
cout <<F[N][V] <<endl;
return 0;
}
空间时间复杂度为O(V*N),可以进一步的优化空间复杂度至O(V)。
首先要知道,我们之前是通过i从1到N的循环来依次表达前i件物品存入的状态。
即i = 1..N,每次计算出二维数组F[i, C[i]..V]的所有值。
那么,如果只用一个数组F[0..V],能不能保证第N次循环结束后F[v]就是我们定义的状态F[N, v]呢?
F[i, v]是由F[i - 1, v]和F[i - 1, v - C[i]]两个子问题递推而来。
能否保证在推F[v]时,能够取用类似F[N - 1, v]和F[N - 1, v - C[N]]的值呢?
分析一下F[v]的含义,表示把前i件物品放入容量为v的背包里所能得到的最大价值。
假设第i件物品不放,则F[v]的含义转化为“把前i - 1件物品放入容量为v的背包里所能得到的最大价值”。
假设第i件物品要放,则问题转化为F[v - C[i]]。
根据前面的递推,F[v]在被更新前(即假设第i件物品不放)相当于F[i - 1, v],我们假设V > v1 > v2 > C[i]。
如果v = C[i]..V,会有一种情况,当v2被物品i更新一次,v1时又由F[v2]与背包更新一次,这样相当于物品i被使用了2次,与题意不合。
所以逆序是关键!
for(i = 1; i <= N; i++)
for(v = V; v >= C[i]; v--)
F[v] = max(F[v], F[v - C[i]] + W[i]);
改进后的代码:
/*
01背包问题优化,空间复杂度为O(V)
F[v] = max(F[v], F[v - Ci] + Wi)
*/
#include <iostream>
using namespace std;
const int num = 100;
#define EMPTY 0 //当背包要求装满时填-65536
int main()
{
freopen("test.in","r",stdin);
//------输入------
int N, V; //有N件物品,一个容量为V的背包
cin >>N >>V;
int C[num],W[num]; //放入第i件物品耗费的空间是C[i],得到的价值是W[i]。
for(int i = 1; i <= N; i++)
cin >>C[i] >>W[i];
int F[num * 10];
for(int i = 0; i <= V; i++)
F[i] = EMPTY;
//当EMPTY = -65536时
//F[0] = 0;
//------处理------
for(int i = 1; i <= N; i++)
for(int v = V; v >= C[i]; v--)
F[v] = F[v] > F[v - C[i]] + W[i] ? F[v] : F[v - C[i]] + W[i];
//------输出------
cout <<F[V] <<endl;
return 0;
}
初始化细节问题
一般有两种问法,第一种没有要求把背包装满,初始化时应该把F[0..V]全部设为0。这个可以理解成,对于任何容量的背包都有一个合法的解“什么都不装”。
第二种要求背包恰好装满,则F[0] = 0以外F[1..V]全部设为-∞,这样就能保证最终得到的F[V]是一种恰好装满背包的最优解。这个可以理解成,只有容量为0的背包可以在“什么都不装”的情况下被恰好装满,其他容量均没有合法的解,属于未定义状态。
理解后可去http://acm.hdu.edu.cn/showproblem.php?pid=2602测试一下,属于不要求把背包装满的问题。