有N件物品和一个容量为V 的背包。放入第i件物品耗费的空间是Ci,得到 的价值是Wi。
求解将哪些物品装入背包可使价值总和最大。
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不 放。
用子问题定义状态:即F[i, v]表示前i件物品恰放入一个容量为v的背包可以 获得的最大价值。
则其状态转移方程便是:
对于“将前i件物品放入容量为v的背包 2 中”这个子问题,若只考虑第i件物品的策略(放或不放),
那么就可以转化 为一个只和前i − 1件物品相关的问题。
如果不放第i件物品,那么问题就转化 为“前i − 1件物品放入容量为v的背包中”,价值为F[i − 1, v];
如果放第i件物 品,那么问题就转化为“前i − 1件物品放入剩下的容量为v − Ci的背包中”,
此时能获得的最大价值就是F[i − 1, v − Ci ]再加上通过放入第i件物品获得的价 值Wi。
#include
#include
using namespace std;
const int maxn = 105;
int dp[maxn][maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
int v;//背包容量
int n;//N件物品
while(scanf("%d %d",&v,&n) != EOF){
for(int i = 1;i <= n; i++){
scanf("%d",&c[i]);
}
for(int i = 1;i <= n; i++){
scanf("%d",&w[i]);
}
//动态规划边界问题
for(int i = 0;i <= n; i++){
dp[i][0] = dp[0][i] = 0;
}
for(int i = 1;i <= n; i++){//枚举物品
for(int j = 1;j <= v; j++){//枚举背包容量
if(j < c[i]){ //当前背包容量放不下这件物品
dp[i][j] = dp[i - 1][j];
}else{ //当前背包容量可以放下这件物品
dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - c[i]] + w[i]);
}
}
}
printf("%d\n",dp[n][v]);
}
return 0;
}
以上方法的时间和空间复杂度均为O(V*N),其中时间复杂已经不能再优化,但是空间复杂度可以优化到O(N)。
先考虑上面讲的基本思路是如何实现的,肯定是有一个主循环 i = 1....N,每次算出来二维数组f[i][0....v]的所有值;
如果只用一个数组f[0...V],能不能保证第i次循环结束后 f[v]中表示的就是我们定义的状态 f[i][v]呢?
f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推出来,能否保证在推f[i][[v]时(即在第i次主循环中推f[v]时)能够得到f[i-1][v]和
f[i-1][v-c[i]]的值呢?事实上,这要求在每次循环中我们以v=V......0的顺序推 f[v],这样才能保证推 f[v]时,f[v-c[i]]保存的是状态 f[i-1][v-c[i]] 的值。
#include
#include
#include
using namespace std;
const int maxn = 105;
int dp[maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
int v;//背包容量
int n;//N件物品
while(scanf("%d %d",&v,&n) != EOF){
for(int i = 0;i < n; i++){
scanf("%d",&c[i]);
}
for(int i = 0;i < n; i++){
scanf("%d",&w[i]);
}
//动态规划边界问题
memset(dp,0,sizeof(dp));
for(int i = 0;i < n; i++){//枚举物品
for(int j = v;j >= c[i]; j--){//枚举背包容量
//当前背包容量可以放下这件物品
dp[j] = max(dp[j],dp[j - c[i]] + w[i]);
}
}
printf("%d\n",dp[v]);
}
return 0;
}
求最优解的背包问题中,有两种问法:
1.求恰好装满背包时的最优解。
初始化时,f[0]=0,f[1...v]为-INF(INF=0x3f3f3f3f),这样最终得到的f[N]是一种恰好装满背包的最优解
2.没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设 为0。
初始化的f数组事实上就是在没有任何物品可以放入背包时的合 法状态。
如果要求背包恰好装满,那么此时只有容量为0的背包,可能的价值为0,只能被nothing“恰好 装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是−∞了。
如果 背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0, 所以初始时状态的值也就全部为0了。
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i], 价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最 大。
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种 物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很 多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最 大权值。仍然可以按照每种物品不同的策略写出状态转移方程:
这跟01背包问题一样有O(VN)个状态需要求解,但求解每个状态的时间已经不是常数了,求解 状态f[i][v]的时间是Θ( v / c[i]),总的复杂度可以认为是Θ(V ×∑ V / c[i]),是比较大的。
#include
#include
using namespace std;
const int maxn = 1005;
int dp[maxn][maxn];
int c[maxn],w[maxn];
int main()
{
int n,v;
while(scanf("%d %d",&v,&n) != EOF){
for(int i = 0;i < maxn; i++){
dp[i][0] = dp[0][i] = c[i] = w[i] = 0;
}
for(int i = 1;i <= n; i++){
scanf("%d",&c[i]);
}
for(int i = 1;i <= n; i++){
scanf("%d",&w[i]);
}
for(int i = 1;i <= n; i++){
for(int j = 1;j <= v; j++){
dp[i][j] = dp[i - 1][j];
if(c[i] <= j){
dp[i][j] = max(dp[i][j],dp[i][j - c[i]] + w[i]);
}
}
}
printf("%d\n",dp[n][v]);
}
return 0;
}
完全背包问题有一个很简单有效的优化:若两件物品i、 j满 足c[i] <= c[j]且w[i] >= w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可 将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方 法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为 有可能特别设计的数据可以一件物品也去不掉。
首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同 的物品中价值最高的是哪个,可以Θ(V + N)地完成这个优化。
//完全背包
#include
using namespace std;
const int maxn=1e4+10;
int n;//n种物品
int v;//背包容量
int c[maxn];
int w[maxn];
int dp[maxn];
int main()
{
while(scanf("%d%d",&v,&n)!=EOF){
for(int i=0;i
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费 的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。 因为对于第i种物品有Mi+1种策略:取0件,取1件……取Mi件。令F[i, v]表 示前i种物品恰放入一个容量为v的背包的最大价值,则有状态转移方程:
另一种好想好写的基本方法是转化为01背包求解:
把第i种物品换成Mi件01 背包中的物品,则得到了物品数为ΣMi的01背包问题。直接求解之,复杂度仍 然是O(V * ΣMi)。
#include
#include
using namespace std;
const int maxn = 5005;
int dp[maxn][maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
int v;//背包容量
int n;//N件物品
int T;
scanf("%d",&T);
while(T--){
scanf("%d %d",&v,&n);
int p=1;
for(int i = 1;i <= n; i++){
int cost,value,num;
scanf("%d%d%d",&cost,&value,&num);
while(num--){
c[p]=cost;
w[p]=value;
p++;
}
}
//动态规划边界问题
for(int i = 0;i <= n; i++){
dp[i][0] = dp[0][i] = 0;
}
for(int i = 1;i <= p; i++){//枚举p件物品
for(int j = 1;j <= v; j++){//枚举背包容量
if(j < c[i]){ //当前背包容量放不下这件物品
dp[i][j] = dp[i - 1][j];
}else{ //当前背包容量可以放下这件物品
dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - c[i]] + w[i]);
}
}
}
printf("%d\n",dp[p-1][v]);
}
return 0;
}
但是我们期望将它转化为01背包问题之后,能够像完全背包一样降低复杂 度。 仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问 题中第i种物品可取的每种策略——取0 . . . Mi件——均能等价于取若干件代换 以后的物品。另外,取超过Mi件的策略必不能出现。
方法是:将第i种物品分成若干件01背包中的物品,其中每件物品有一个系 数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数 分别为1, 2, 2 ^2 . . . 2 ^ (k−1) , Mi − 2^ (k ) + 1,且k是满足Mi − 2 ^ k + 1 > 0的最大整数。
例 如,如果Mi为13,则相应的k = 3,这种最多取13件的物品应被分成系数分别 为1, 2, 4, 6的四件物品。 分成的这几件物品的系数和为Mi,表明不可能取多于Mi件的第i种物品。另 外这种方法也能保证对于0 . . . Mi间的每一个整数,均可以用若干个系数的和表 示。这里算法正确性的证明可以分0 . . . 2 ^ (k−1)和2 ^ k . . . Mi两段来分别讨论得出, 这样就将第i种物品分成了O(logMi)种物品,将原问题转化为了复杂度 为O(V * ΣlogMi) 的01背包问题,是很大的改进。
#include
#include
using namespace std;
const int maxn = 5005;
int dp[maxn][maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
int v;//背包容量
int n;//N件物品
int T;
scanf("%d",&T);
while(T--){
scanf("%d %d",&v,&n);
int p=1;
for(int i = 1;i <= n; i++){
int cost,value,num;
scanf("%d%d%d",&cost,&value,&num);
int k=1;
while(num-(k*2)+1>0){
c[p]=k*cost;
w[p]=k*value;
k*=2;
p++;
}
k=(num-k+1);
c[p]=k*cost;
w[p]=k*value;
p++;
}
//动态规划边界问题
for(int i = 0;i <= n; i++){
dp[i][0] = dp[0][i] = 0;
}
for(int i = 1;i <= p; i++){//枚举p件物品
for(int j = 1;j <= v; j++){//枚举背包容量
if(j < c[i]){ //当前背包容量放不下这件物品
dp[i][j] = dp[i - 1][j];
}else{ //当前背包容量可以放下这件物品
dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - c[i]] + w[i]);
}
}
}
printf("%d\n",dp[p-1][v]);
}
return 0;
}