背包三连(01背包 + 多重背包 + 完全背包)

  01背包问题

  题目:有n件物品需要放入一个容量为v的背包,第i件物品的体积为vi,他的价值为wi,求解将哪些物品装入背包可以使得总价值最大。

  题目特点:每种物品只有一件,可以选择或者不放。

  用子问题定义状态:即用dp[ i ][ j ] 表示前i件物品恰放入一个容量为 j 的背包可以获得的最大价值。则其状态转移方程为:dp[ i ][ j ] = max(dp[i - 1][ j ], dp[i - 1][j - vi] + w[ i ]);

    下面将这个方程解释一下,将前i个物品放入容量为 j 的背包中这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为“前i - 1件物品放入容量为 j 的背包中,此时产生的

价值为dp[ i - 1] [ j ],如果放入第 i 件物品,那么问题就转化为前 i - 1 件物品放入剩下的容量为j - ci 的背包中,此时能获得的最大价值为dp[ i - 1][ j - c[i] ]再加上通过第 i 件物品获得的价值w[ i ]。

  滚动数组优化:我们在计算都是利用dp[ i - 1 ][ 0 ..... v] 计算出了dp[ i ][ 0 .... v] ,由上式可以看出dp[ i ]只与dp[ i - 1]有关,那么我们可不可以省略这一组呢?答案是可以的,如果我们省略第一

维,那么我们就可以得出另一个状态转移方程dp[ j ] = max(dp[ j ], dp[ j - v[ i ] ] + w[ i ] )。在这里我们的状态转移方程就相当于dp[ i ][ j ] = max(dp[i - 1][ j ], dp[i - 1][j - vi] + w[ i ]);那么在二维状态下

我们利用dp[ i - 1] [ j - v[ i ] ] + w[ i ] 推出了dp [ i ][ j ],这里第二维是从小到大推的,所以倒序和顺序不影响结果,但是如果我们将第一维度去掉这样还可以么?我们可以考虑,如果 j 依然是从v [ i ] - > V还可行么,

答案是不可行,因为如果还是顺序访问的话,我们只是用dp[ i ][j - v[ i ] ] + w[ i ] 推出了dp [ i ][ j ],这与原题不符合。

  初始化:01背包问题一般分为两种,第一种为恰好装满背包,第二种为给定背包求解能带走的最大值而不必恰好装满。在求解恰好装满的背包中我们需要将dp[ 0 ] 初始化为0,其余的都初始化为 -INF,

为啥要这样呢,由于需要得到将背包完全装满的状态,所以我们将象征着背包装满的dp[ 0 ]初始化为0,其余的皆为-INF,这样的话只有达到刚好装满状态的装载方法才会依次滚动下来,未装满的情况并不会被保留。

  完全背包

  题目:有n件物品需要放入一个容量为v的背包,第i件物品的体积为vi, 他的价值为wi,每件物品都有无限多个,求解将哪些物品放入背包可以使得背包中的总价值最大。

  题目特点:每种物品有无穷件。

  和01背包不一样的是所有物品都有不止一件,所以我们很容易可以得出状态转移方程:dp[ i ][ j ] = max(dp[i - 1][j - k * vi] + k * w[ i ])(0 <= k * c[ i ] << v);参照01背包的方程这个式子也很容易得出。

  转换为01背包问题:我们可以知道对于每一种物品,我们把他拆分成1 ~ V / c[ i ]件同样的物品,然后对于所有物品我们进行与01背包相同的计算即可得出正确答案。

  我们可以给出该思路的伪代码:

  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的循环次序不同而已。 为什么这个算法就可行呢?首先想想为什么01背包中要按照v递减的次序来 循环。让v递减是为了保证第i次循环中的状态F[i, v]是由状态

F[i − 1, v − Ci ]递 推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入 第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F[i − 1, v − Ci ]。而现在完全背包的特点恰是每种物

品可选无限件,所以在考虑“加 选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果F[i, v − Ci ],所以就可以并且必须采用v递增的顺序循环。这就是这个简单的 程序为何成立的道理。 值得一

提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有 可能会带来算法时间常数上的优化。 这个算法也可以由另外的思路得出。例如,将基本思路中求解F[i, v−Ci ]的 状态转移方程显式地写出来,代入

原方程中,会发现该方程可以等价地变形成 这种形式: F[i, v] = max(F[i − 1, v], F[i, v − Ci ] + Wi)。

  多重背包

  题目:有n件物品需要放入一个容量为v的背包,第i件物品的体积为vi, 他的价值为wi,每件物品都有m[ i ] 个,求解将哪些物品放入背包可以使得背包中的总价值最大。

  题目特点:每种物品有有限多件。

  这个问题和完全背包很相似,我们只需要将完全背包的代码稍作改动即可得出dp[ i ][ j ] = max(dp[i - 1][j - k * vi] + k * w[ i ])(0 <= k <= m [ i ]);

  转化为01背包问题:我们依然可以像完全背包一样将所有物品拆分为1~m[ i ]件物品,这样做的效率等同于上面的方法,所以我们依旧考虑二进制拆分。

  将第 i 种物品我们仍然进行拆分,拆分之后的的物品的价值为k * w[ i ],体积为k * v[ i ],k取1, 2, 4, ....2^(k - 1), m[ i ] - 2 ^k + 1。且是满足m [ i ] - 2^k + 1 > 0的最大整数。这样我们就将原本的m[ i ] 件物品拆分为log m件物品,

在复杂度上是一个较大的提升。

  下面是总体的参考代码:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int maxn = 1000 + 5, maxe = 1e4 + 5;
 6 int n, V, w[maxn],d[maxn], dp[maxe];
 7 
 8 void zero_one(int w, int d, int v) {
 9     for(int j = v; j >= w; j --)
10         dp[j] = max(dp[j], dp[j - w] + d);
11 }
12 
13 void complete_pack(int w, int d, int v) {
14     for(int j = w; j <= v; j ++)
15         dp[j] = max(dp[j], dp[j - w] + d);
16 }
17 
18 void MutiplePack(int w, int d, int v, int m) {
19     if(m * w >= v) {
20         complete_pack(w, d, v);
21         return;
22     }
23     int k = 1;
24     while(k <= m) {
25         zero_one(k * w, k * d, v);
26         m -= k;
27         k *= 2;
28     }
29     zero_one(w * m, m * d, v);
30 }
31 
32 int main () {
33     scanf("%d %d", &n, &V);
34     for(int i = 0; i < n; i ++)
35         scanf("%d %d", &w[i], &d[i]);
36     return 0;
37 }

猜你喜欢

转载自www.cnblogs.com/bianjunting/p/10585185.html