【01背包】
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
01背包:f[i][j] :前i个物品,其容量为j时的最大价值
for ( 1~n ): 依次枚举所有物品
for ( 0 ~ m ): 枚举背包的容量
if ( j >= v[i] ): 如果当前的物品能放进背包
f[i][j] = max( f[i-1][j] , f[i-1][j-v[i]]+w[i])
不选 选
同时这里有一个空间上的优化,主要是基于每次的物品放完后,只有对应的内容进行更新,如果放下一个物品的时候,其实只需要更新上一个状态即可。
01背包,只允许放一次,所以更新的时候还需要 for循环枚举的顺序还需要注意:
for( int j = V ; j >= v[i] ; j-- )
f[j] = max( f[j] , f[j-v[i]] + w[i] )
该状态 第i个物品还没有被覆盖,
∴ f[j-v[i]]实际就是指第i-1个物品时的状态
∴ 这里其实 从后往前历遍,每次只询问一回,而且是基于前i-1个物品更新后的值,所以等价于只是放了一遍
01背包模板:
1 #include<iostream>
2 using namespace std;
3
4 const int N = 1e3+5;
5
6 int f[N],v[N],w[N];
7 int n,m;
8
9 int main()
10 {
11 cin >> n >> m ;
12 for(int i=0;i<n;i++){
13 cin >> v[i] >> w [i];
14 }
15 for(int i=0;i<n;i++){
16 for(int j=m;j>=v[i];j--){
17 f[j] = max(f[j],f[j-v[i]]+w[i]);
18 }
19 }
20 cout << f[m] << endl;
21 }
观察上面的代码:
1、请问为什么 f[m]就是最终答案?(可能放置的物品容量还没有到达m)
原因是:与初始化有关,因为初始化为0,例如 在 容量为k的时候就已经把背包的价值达到最大了, f[j] = max( f[j] , f[j-v[i]]+w[i] )
该过程 k+1 从0开始放物品,但其实最后放的物品的种类与f[k]等价的。
k , k+1 , …… m
∴f[m]此时为答案。
2、若想把容量恰好为m的最大价值?
需要对初始化动手脚,初始化f[0]=0,f[其他] = -inf
完全背包
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
f[i]表示总体积为i 时的最大价值
最朴素的做法:基于01背包做法
for( i = 1 ; i <= n ; i++ ) 依次枚举所有物品
for( j = V ; j >= v[i] ; j-- ) 枚举当前容量
for( k = 0 ; k*v[i] <= j ; k++ ) 枚举容量下可以放多少个,若能放下的前提下更新
f[j] = max( f[j] , f[j-k*v[i]]+k*w[i] )
优化后:
for( i = 1 ; i <= n ; i++ )
for( j = v[i] ; j <= V ; j++ ) //注意枚举的顺序
f[j] = max( f[j] , f[j-v[i]]+w[i])
其实在当前位置的j的时候,容量为:j-v[i],可能已经利用该物品进行更新了。
效果为:多次选择该物品。
证明:
数学归纳法:
1、假设考虑i-1个物品后,所有的f[j]都是正确的
2、来证明,考虑第i个物品后,所有的f[j]也都是正确的
对于某个j而言,最优解包含k个v[i]
f[j-k*v[i]]
f[j-(k-1)*v[i]]+w[i]
……………………
f[j-v[i]] + w[i]
完全背包模板
1 #include<iostream>
2 using namespace std;
3 const int N = 1e3+10;
4
5 int f[N],v[N],w[N];
6
7 int n,m;
8
9 int main()
10 {
11 cin >> n >> m ;
12 for(int i=0;i<n;i++){
13 cin >> v[i] >> w[i] ;
14 }
15 for(int i=0;i<n;i++){
16 for(int j=v[i];j<=m;j++){
17 f[j] = max( f[j] , f[j-v[i]]+w[i]);
18 }
19 }
20 cout << f[m] << endl;
21 return 0;
22 }
多重背包
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。
朴素做法:
for(int i=1;i<=n;i++)//枚举物品
for(int j=V;j>=0;j--)//枚举体积
for(int k=1;k<=num[i],k++)
//这个枚举到num[i]
if(j-k*c[i]>=0)//判断能否装下.
f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
二进制优化
二进制拆分的原理
我们可以用 1,2,4,8...2^n 表示出 1 到 2^{n+1}-1的所有数.
考虑我们的二进制表示一个数。
根据等比数列求和,我们很容易知道我们得到的数最大就是 2^{n+1}-1
而我们某一个数用二进制来表示的话,每一位上代表的数都是 2 的次幂.
就连奇数也可以,例如-> 19 可以表示为 10011 (2)
二进制拆分的做法
因为我们的二进制表示法可以表示从 1 到 num[i] 的所有数,我们对其进行拆分,就得到好多个大物品(这里的大物品代表多个这样的物品打包得到的一个大物品).
(简单来讲,我们可以用一个大物品代表 1,2,4,8.. 件物品的和。)
而这些大物品又可以根据上面的原理表示出其他不是2的次幂的物品的和.
因此这样的做法是可行的.
我们又得到了多个大物品,所以再去跑01背包即可.
二进制优化的模板:
1 #include<iostream>
2 using namespace std;
3
4 const int N = 2e3+10;
5
6 int f[N],v[N],w[N],s[N];
7
8 int n,m;
9
10 int main()
11 {
12 cin >> n >> m;
13 for(int i=0;i<n;i++){
14 cin >> v[i] >> w[i] >> s[i] ;
15 }
16 for(int i=0;i<n;i++){
17
18 for(int k=0 ; (1<<k) <= s[i] ; k++ ){
19 int K = (1<<k);
20 for(int j=m ; j>=K*v[i] ; j-- ){
21 f[j] = max( f[j] , f[j-K*v[i]] + K*w[i] );
22 }
23 s[i] = s[i] - (1<<k);
24 }
25 for(int j=m ; j >= s[i]*v[i] ; j-- ){
26 f[j] = max( f[j] , f[j-s[i]*v[i]] + s[i]*w[i] );
27 }
28 }
29 cout << f[m] << endl ;
30 return 0 ;
31 }