一,01背包问题
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
有N件物品和一个容量为V 的背包。放入第i件物品耗费的费用是Ci1,得到 的价值是Wi。求解将哪些物品装入背包可使价值总和最大。 也即占用背包的空间容量,后文统一称之为“费用(cost)”
用子问题定义状态:即F[i,v]表示前i件物品恰放入一个容量为v的背包可 以获得的最大价值。则其状态转移方程便是: F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi} //前i-1件背包的容量为v,放入第i件物品需要v减去第i件物品的的费用再加上第i件物品的价值
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生 出来的。
所以有必要将它详细解释一下:“将前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}
1.3经过优化空间复杂度的伪代码
F[0..V ]←0
for i ← 1 to N
for v ← V to Ci//经过了逆序的思路
F[v] ← max{F[v],F[v−Ci] + Wi}
为什么这个算法就可行呢?首先想想为什么01背包中要按照v递减的次序来 循环。让v递减是为了保证第i次循环中的状态F[i,v]是由状态F[i−1,v −Ci]递 推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入 第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F[i− 1,v −Ci]。
1.4 初始化的细节问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。 有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背 包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。 如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其 它F[1..V ]均设为−∞,这样就可以保证最终得到的F[V ]是一种恰好装满背包的 最优解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该 将F[0..V ]全部设为0。 这是为什么呢?可以这样理解:初始化的F数组事实上就是在没有任何物 品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量 为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的 背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。如果背包并非 必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的 价值为0,所以初始时状态的值也就全部为0了。
例题
1033.采药
Description
Input
Output
Sample Input
100 5 77 92 22 22 29 87 50 46 99 90
Sample Output
133
源代码如下,(已经AC过的)
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
int main()
{
int T,M,v;
int f[10005],c[10005],w[10005];
while(scanf("%d %d",&T,&M)!=EOF)
{
memset(f,0,sizeof(f));
memset(c,0,sizeof(c));
memset(w,0,sizeof(w));
for(int i=0; i<M; i++)
{
scanf("%d %d",&c[i],&w[i]);
}
for(int i=0; i<M; i++)
{
for(int j=T; j>=c[i]; j--)//背包的容量逐渐减少
{
if(f[j-c[i]]+w[i] > f[j])//放入第i件与不放第i件的总价值进行比较
{
f[j] = f[j-c[i]] + w[i];
}
}
}
cout<<f[T]<<'\n';
}
return 0;
}
杭电上有很多关于01背包的问题,有意向的可以到杭电上去搜索练习
二,完全背包问题
完全背包和01背包的差别在于完全背包中每件物品都有无限件可用
2.2 基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就 是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有 取0件、取1件、取2件……直至取⌊V /Ci⌋件等许多种。 如果仍然按照解01背包时的思路,令F[i,v]表示前i种物品恰放入一个容 量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方 程,像这样:
F[i,v] = max{F[i−1,v−kCi] + kWi |0 ≤ kCi ≤ v}
这跟01背包问题一样有O(V N)个状态需要求解,但求解每个状态的时 间已经不是常数了,求解状态F[i,v]的时间是O( v Ci ),总的复杂度可以认为 是O(NV Σ V Ci ),是比较大的。 将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明 01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是 要试图改进这个复杂度。
2.5 O(V N)的算法
这个算法使用一维数组,先看伪代码:
F[0..V ]←0
for i ← 1 to N
for v ← Ci to V
F[v] ← max(F[v],F[v−Ci] + Wi)
你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加 选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结 果F[i,v−Ci],所以就可以并且必须采用v递增的顺序循环。这就是这个简单的 程序为何成立的道理。
例题
1043.采药2
Description
Input
Output
Sample Input
100 5 77 92 33 50 34 60 50 46 99 161
Sample Output
161
源代码(已经AC过的)
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int main()
{
int m,n;
int c[10005],v[10005],f[10005];
scanf("%d %d",&m,&n);
memset(c,0,sizeof(c));
memset(v,0,sizeof(v));
memset(f,0,sizeof(f));
for(int i=0;i<n;i++)
{
scanf("%d %d",&c[i],&v[i]);
}
for(int i=0;i<n;i++)
{
for(int j=c[i];j<=m;j++)
{
if(f[j-c[i]]+v[i]>f[j])
{
f[j]=f[j-c[i]]+v[i];
}
}
}
cout<<f[m]<<'\n';
return 0;
}