背包问题集锦 | 青训营

81 阅读3分钟

【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 }