题目:
有 N 件物品和一个容量为 V 的背包。第 i 件物品的费用是 w[i],价值是 p[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
本文按照动态规划的标准模式解析:http://blog.csdn.net/hearthougan/article/details/53749841
0-1背包问题,表示的是每个物品只有一件,每件物品不能分割,在不超过背包容量的同时,如何选取物品,使得背包所装的价值最大(背包可以装不满)。这是一个经典的动态规划问题,有《背包九讲》珠玉在前,我所能做的也只是按自己的理解,加以分析这个问题。如果需要《背包九讲》请点击:
http://download.csdn.net/detail/hearthougan/5891267
不妨设集合表示n个物品,该问题的某一个最优解集合为
。
1、最优子结构
我们已经假设该问题的最优解为A,那么对于某个物品,令
,表示原问题除去物品
后的一个子问题,那么
,就是该子问题的一个最优解。可反证此问题,即存在一个
是子问题的最优解解,那么
,所得的集合
一定比原问题最优解集合S所得的最大价值要大,这与假设矛盾。因此原问题的最优解一定包含子问题的最优解,这就证明了最优子结构性质。
2、递归地定义最优解的值
对于每个物品我们可以有两个选择,放入背包,或者不放入,有n个物品,故而我们需要做出n个选择,于是我们设f[i][v]表示做出第i次选择后,所选物品放入一个容量为v的背包获得的最大价值。现在我们来找出递推公式,对于第i件物品,有两种选择,放或者不放。
<1>:如果放入第i件物品,则f[i][v] = f[i-1][v-w[i]]+p[i],表示,前i-1次选择后所选物品放入容量为v-w[i]的背包所获得最大价值为f[i-1][v-w[i]],加上当前所选的第i个物品的价值p[i]即为f[i][v]。
<2>:如果不放入第i件物品,则有f[i][v] = f[i-1][v],表示当不选第i件物品时,f[i][v]就转化为前i-1次选择后所选物品占容量为v时的最大价值f[i-1][v]。则:
f[i][v] = max{f[i-1][v], f[i-1][v-w[i]]+p[i]}
3、求解最优值
我们根据2的递推公式,可以实现代码如下:
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int MAXN = 100;
int main()
{
int n, V;
int f[MAXN][MAXN];
int w[MAXN], p[MAXN];
while(cin>>n>>V)
{
if(n == 0 && V == 0)
break;
for(int i = 0; i <= n; ++i)
{
w[i] = 0, p[i] = 0;
for(int j = 0; j <= n; ++j)
{
f[i][j] = 0;
}
}
for(int i = 1; i <= n; ++i)
cin>>w[i]>>p[i];
for(int i = 1; i <= n; ++i)
{
for(int v = 1; v <= V; ++v)
{
if(v-w[i]>=0)
f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+p[i]);
else
f[i][v]=f[i-1][v];
}
}
cout<<f[n][V]<<endl;
}
return 0;
}
由此我们可以知道,我们必须要做出n次选择,所以外层n次循环是必不可少的,对于上面代码的内层循环,表示当第i个商品要放入容量为v(v = w[i]....V)的背包时所获得的价值,即先对子问题求解,这个也是必不可少的,所以时间复杂度为O(nV),这个已不能进一步优化,但是我们可以对空间进行优化。
由于我们用f[n][V]表示最大价值,但是当物品和背包容量比较大时,这种用法会占用大量的空间,那么我们是不是对此可以进一步优化呢?
现在考虑如果我们只用一位数组f[v]来表示f[i][v],是不是同样可以达到效果?我们由上述可知f[i][v]是由f[i-1][v]和f[i-1][v-w[i]]这两个子问题推得,事实上第i次选择时,我们虽用到前i-1次的最优结果,但是前i-1次选择的最优结果,已经保存在做出第i-1次选择后的结果中,即第i次的结果只用到了第i-1次选择后的状态,因此我们可以只用一维数组来维持每次选择的结果,怎么维持?也就是当第i次选择时,我们怎么得到f[i-1][v]和f[i-1][v-w[j]]这两种状态,即第i次求f[v]时,此时f[v]和f[v-w[i]]表示的是不是f[i-1][v]和f[i-1][v-w[j]],事实上我们只需要将内层循环变为从V到w[j]的逆向循环即可满足要求。这句话不是很好理解,我们先给出优化后的代码,然后由图表来慢慢分析。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
const int MAXN = 100;
int main()
{
int n, V;
int f[MAXN];
int w[MAXN], p[MAXN];
while(cin>>n>>V)
{
if(n == 0 && V == 0)
break;
memset(f, 0, sizeof(f));
memset(w, 0, sizeof(w));
memset(p, 0, sizeof(p));
for(int i = 1; i <= n; ++i)
cin>>w[i]>>p[i];
for(int i = 1; i <= n; ++i)
{
for(int v = V; v >= w[i]; --v)
{
f[v] = max(f[v], f[v-w[i]]+p[i]);
}
}
cout<<f[V]<<endl;
}
return 0;
}
例,有3个物品,背包容量为10,如下:
初始时:我们初始化f[]全部为0
第1次主循环,即当i = 1时,我们只对物品1进行选择,对于内层循环,即当v = 10....3时,我们有:
f[10] = max{f[10], f[10-3]+p[1]} = max{f[10], f[7]+4} = max{0, 0+4} = 4;
f[9] = max{f[9], f[9-3]+p[1]} = max{f[9], f[6]+4} = max{0, 0+4} = 4;
f[8] =max{f[8], f[8-3]+p[1]} = max{f[8], f[5]+4} = max{0, 0+4} = 4;
f[7] = max{f[7], f[7-3]+p[1]} = max{f[7], f[4]+4} = max{0, 0+4} = 4;
f[6] = max{f[6], f[6-3]+p[1]} = max{f[6], f[3]+4} = max{0, 0+4} = 4;
f[5] = max{f[5], f[5-3]+p[1]} = max{f[5], f[2]+4} = max{0, 0+4} = 4;
f[4] = max{f[4], f[4-3]+p[1]} = max{f[4], f[1]+4} = max{0, 0+4} = 4;
f[3] = max{f[3], f[3-3]+p[1]} = max{f[3], f[0]+4} = max{0, 0+4} = 4;
f[2] = f[1] = f[0] = 0;
其中f[2] = f[1] = f[0] = 0,是因为体积为3的物品,根本不会影响当背包容量为2、1、0时的状态。所以他们依旧保持原来的状态。对应于:
表中横轴的蓝色区域,表示当容量为v时,对第1个商品做出选择时所依赖的上一层的状态,如当v=10时,所依赖的就是f[0][10]和f[0][7]两个状态。所以当计算f[1][v](v = 10....3)时,f[v]和[v-3]保存的就是f[0][v]和f[0][v-3]。
第2次循环,即i = 2时,我们对物品2做出选择:
f[10] = max{f[10], f[10-4]+p[2]} = max{f[10], f[6]+p[2]} = max{4, 4+5} = 9
f[9] = max{f[9], f[9-4]+p[2]} = max{f[9], f[5]+p[2]} = max{4, 4+5} = 9
f[8] = max{f[8], f[8-4]+p[2]} = max{f[8], f[4]+p[2]} = max{4, 4+5} = 9
f[7] = max{f[7], f[7-4]+p[2]} = max{f[7], f[3]+p[2]} = max{4, 4+5} = 9
f[6] = max{f[6], f[6-4]+p[2]} = max{f[6], f[2]+p[2]} = max{4, 0+5} = 5
f[5] = max{f[5], f[5-4]+p[2]} = max{f[5], f[1]+p[2]} = max{4, 0+5} = 5
f[4] = max{f[4], f[4-4]+p[2]} = max{f[4], f[0]+p[2]} = max{4, 0+5} = 5
f[3] = 4
f[2] = f[1] = f[0] = 0;
第3次循环,即当i = 3时
f[10] = max{f[10], f[10-5]+p[3]} = max{f[10], f[5]+3} = max{9, 5+6} = 11
f[9] = max{f[9], f[9-5]+p[3]} = max{f[9], f[4]+3} = max{9, 5+6} = 11
f[8] = max{f[8], f[8-5]+p[3]} = max{f[8], f[3]+3} = max{9, 4+6} = 10
f[7] = max{f[7], f[7-5]+p[3]} = max{f[7], f[2]+3} = max{9, 0+6} = 9
f[6] = max{f[6], f[6-5]+p[3]} = max{f[6], f[1]+3} = max{5, 0+6} = 6
f[5] = max{f[5], f[5-5]+p[3]} = max{f[5], f[0]+3} = max{5, 0+6} = 6
f[4] = 5
f[3] = 4
f[2] = f[1] = f[0]
对于应表:
综上的步骤我们可以很清楚的知道v从大到小循环可以满足题意,从表中我们也可以知道,我们如用f[i][v]存储最优结果,有很多没用的求解结果也被保存下来,从而浪费了大量的空间。如果我们用f[v],那么保存的就是最终结果,且很好地利用了空间。
如果还是疑问为什么v从小到大的顺序不可以(即内循环为:for(int v = w[i]; v <= V; ++v)),我们依旧可以按着代码,试着画一下表,一次,就很清楚了,比如同样的问题,正着走一次为:
当i=1时:
f[0] = f[1] = f[2] = 0
f[3] = max{f[3], f[3-3]+p[1]} = max{0, 0+4} = 4
f[4] = max{f[4], f[4-3]+p[1]} = max{0, 0+4} = 4
f[5] = max{f[5], f[5-3]+p[1]} = max{0, 0+4} = 4
以上结果貌似是对的,但是意思已经完全不是我们当初设计代码时的思想了,注意下面:
f[6] = max{f[6], f[6-3]+p[1]} = max{0. 4+4} = 8//此时我们用的f[6]和f[3]相当于是f[1][6]和f[1][3],而不是f[0][6]和f[0][3]!
f[7] = max{f[7], f[7-3]+p[1]} = max{0, 4+4} = 8
f[8] = max{f[8], f[8-3]+p[1]} = max{0, 4+4} = 8
f[9] = max{f[9], f[9-3]+p[1]} = max{0, 8+4} = 12
f[10] = max{f[10], f[10-3]+p[1]} = max{0, 8+4} = 12
到此,我们清楚了为什么正向循环为何不可,因为此时f[v]保存的相当于是f[i][v]和f[i][v-w[i]],这已经违背了我们意图!
转自:https://blog.csdn.net/Hearthougan/article/details/53869671