• 背包三连(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,这样的话只有达到刚好装满状态的装载方法才会依次滚动下来,未装满的情况并不会被保留。

      参考代码:

    1 void Zero_One_Pick() {//n为物品个数,V为背包容量,v[i]为每件物品所占得体积,w[i]为他所带来的价值
    2     for(int i = 1; i <= n; i ++)
    3         for(int j = V; j >= v[i]; j --)
    4             dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    5 }

      完全背包

      题目:有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背包相同的计算即可得出正确答案。但是这样做并不会给问题带来时间复杂度上的

    优化,那么我们可以考虑另一种拆分方式,可以将第 i 件物品拆分为体积为v[ i ] * 2 ^ k,所带来的价值为w[ i ] * 2^k的多个物品,这里v[ i ] * 2 ^ k <= V;我们可以知道,对于物品 i 组成的每一种拆分情况我们都可以利用

    对应的二进制得到,所以显而易见我们算法得正确性。

      我们可以给出一维数组滚动情况下该思路的伪代码:

      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)。

    下面给出该算法的参考代码:

    1 void Complete_Pick() {
    2      for(int i = 0; i <= n; i ++) {
    3          for(int j = v[i]; j <= V; j ++) {
    4              dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    5          }     
    6     }          
    7 }    

      多重背包

      题目:有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 种物品我们仍然进行拆分,拆分之后的的物品 i 的价值为k * w[ i ],体积为k * v[ i ],k取1, 2, 4, ....2^(k - 1), m[ i ] - 2 ^k + 1 > 0。即 k 是满足m [ i ] - 2^k + 1 > 0的最大整数。这样我们就将原本的m[ i ] 件

    物品拆分为log m件物品,我们很容易可以知道我们新切分的这些物品可以组成物品 i 的状态数的所有状态,即0......m[ i ]。这样使得算法的复杂度得到了较大的提升。

      下面我们给出该算法的参考代码:

     1 #include <cstdio>
     2 #include <algorithm>
     3 using namespace std;
     4 
     5 const int maxn = 1000 + 5, maxe = 1e4 + 5;
     6 int n, V, v[maxn], w[maxn], dp[maxe], c[maxn];
     7 
     8 void Mutiple_Pack() {
     9     for(int i = 0; i < n; i ++) {
    10         if(c[i] * v[i] >= V) {//如果第i件物品的总质量大于等于V则视这种物品为无限提供
    11             for(int j = v[i]; j <= V; j ++)
    12                 dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    13             continue;
    14         }
    15         int k = 1, num = c[i];
    16         while(k <= num) {
    17             for(int j = V; j >= k * v[i]; j --)
    18                 dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
    19             num -= k;
    20             k *= 2;
    21         }
    22         for(int j = V; j >= num * v[i]; j --)
    23             dp[j] = max(dp[j], dp[j - num * v[i]] + num * w[i]);
    24     }
    25 }
    26 
    27 int main () {
    28     scanf("%d %d", &n, &V);
    29     for(int i = 0; i < n; i ++)
    30         scanf("%d %d", &v[i], &w[i]);
    31     Mutiple_Pack();
    32     printf("%d
    ", dp[V]);
    33     return 0;
    34 }

      针对填满多重背包的单调队列优化的O(VN)的优化算法:

      单调队列已经在之前的一篇博客中讲到,这里不再赘述,这是之前博客的链接https://www.cnblogs.com/bianjunting/p/10566469.html

      

  • 相关阅读:
    Netsharp快速入门(之17) Netsharp基础功能(参照高级设置)
    Netsharp快速入门(之16) Netsharp基础功能(权限管理)
    安装 SQL SERVER PROFILER
    运用 DataContractSerializer 存储本地对象
    坑人的 try catch finally
    截图库
    Asp.Net MVC 过滤器
    Application、Session、Cookie、ViewState的特性
    Ioc 比较
    Redis 安装与配置
  • 原文地址:https://www.cnblogs.com/bianjunting/p/10585185.html
Copyright © 2020-2023  润新知