• 动态规划之背包问题


    动态规划的基本思想:

    将一个问题分解为子问题递归求解,且将中间结果保存以避免重复计算。通常用来求最优解,且最优解的局部也是最优的。求解过程产生多个决策序列,下一步总是依赖上一步的结果,自底向上的求解。

    动态规划算法可分解成从先到后的4个步骤:

    1. 描述一个最优解的结构,寻找子问题,对问题进行划分。

    2. 定义状态。往往将和子问题相关的各个变量的一组取值定义为一个状态。某个状态的值就是这个子问题的解(若有k个变量,一般用K维的数组存储各个状态下的解,并可根据这个数组记录打印求解过程。)。

    3. 找出状态转移方程。一般是从一个状态到另一个状态时变量值改变。

    4. 以“自底向上”的方式计算最优解的值。

    5. 从已计算的信息中构建出最优解的路径。(最优解是问题达到最优值的一组解)

    其中步骤1~4是动态规划求解问题的基础,如果题目只要求最优解的值,则步骤5可以省略。

    背包问题

    01背包: 有N件物品和一个重量为M的背包。(每种物品均只有一件)第i件物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使价值总和最大。

    完全背包: 有N种物品和一个重量为M的背包,每种物品都有无限件可用。第i种物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包重量,且价值总和最大。

    多重背包: 有N种物品和一个重量为M的背包。第i种物品最多有n[i]件可用,每件重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的总量总和不超过背包重量,且价值总和最大。

     

    01背包问题:

    这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

    用子问题定义状态:即c[i][m]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值。则其状态转移方程便是:

    c[i][m]=max{c[i-1][m],c[i-1][m-w[i]]+p[i]}

    这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入重量为m的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为m的背包中”,价值为c[i-1][m];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的重量为m-w[i]的背包中”,此时能获得的最大价值就是c[i-1][m-w[i]]再加上通过放入第i件物品获得的价值p[i]。

    测试数据:

    10,3

    3,4

    4,5

    5,6

     

     

    c[i][j]数组保存了1,2,3号物品依次选择后的最大价值.

    这个最大价值是怎么得来的呢?从背包容量为0开始,1号物品先试,0,1,2,的容量都不能放.所以置0,背包容量为3则里面放4.这样,这一排背包容量为4,5,6,....10的时候,最佳方案都是放4.假如1号物品放入背包.则再看2号物品.当背包容量为3的时候,最佳方案还是上一排的最价方案c为4.而背包容量为4的时候,则最佳方案为自己的重量5.背包容量为7的时候,很显然是5加上一个值了。加谁??很显然是7-4=3的时候.上一排c3的最佳方案是4.所以。总的最佳方案是5+4为9.这样.一排一排推下去。最右下放的数据就是最大的价值了。(注意第3排的背包容量为7的时候,最佳方案不是本身的6.而是上一排的9.说明这时候3号物品没有被选.选的是1,2号物品.所以得9.)

     

    #include <iostream>
    
    using namespace std;
    
    const int MAXN = 100;
    const int MAXM = 1000;
    
    /**
     * 01背包
     * @param n 物品个数
     * @param m 背包承重量
     * @param p[] 物品价值
     * @param w[] 物品重量
     * @return 最大价值
     */
    int knapsack01(int n, int m, int p[], int w[])
    {
        int c[MAXN][MAXM];
        for(int i=1; i<=n; ++i)
        {
            c[i][0] = 0;
        }
    
        for(int i=1; i<=m; ++i)
        {
            c[0][i] = 0;
        }
    
        for(int i=1; i<=n; ++i)
        {
            for(int j=1; j<=m; ++j)
            {
    
                if((w[i]<=j) && (c[i-1][j] < c[i-1][j-w[i]]+p[i]))
                {
                    c[i][j] = c[i-1][j-w[i]]+p[i];
                }
                else
                {
                    c[i][j] = c[i-1][j];
                }
            }
        }
    
        return c[n][m];
    }
    
    
    
    
    
    int main(int argc, char** argv)
    {
    
        int p[MAXN];
        int w[MAXM];
        p[1] = 4;
        p[2] = 5;
        p[3] = 6;
        w[1] = 3;
        w[2] = 4;
        w[3] = 5;
    
        cout << knapsack01(3,10,p,w) <<endl;
    
    
    
        return 0;
    }

    这里是用二位数组存储的,可以把空间优化,用一位数组存储。

    用f[0..m]表示,f[m]表示把前i件物品放入容量为m的背包里得到的价值。把i从1~n(n件)循环后,最后f[m]表示所求最大值。

    *这里f[m]就相当于二位数组的c[i][m]。那么,如何得到c[i-1][m]和c[i-1][m-w[i]]+p[i]?

    首先要知道,我们是通过i从1到n的循环来依次表示前i件物品存入的状态,即:for i=1..N。 现在思考如何能使f[m]表示当前状态是容量为m的背包所得价值,而又使f[m]和f[m-w[i]]+p[i]表示前一状态的价值?

    逆序!

    for i=1..N
    
       for m=M..0
    
            f[m]=max{f[m],f[m-w[i]]+p[i]};

    这里为什么要逆序呢?想想max中比较的两部分,对于f[m],当前i情况下的f[m]值还没有求出来,所以f[m]中存储的必然是i-1情况下的值,即为原来的c[i-1][m];但是f[m-w[i]]就不同了,如果是正序,当前i情况下,在求f[m]之前,很可能f[m-w[i]]已经计算出值了,所以为了保证f[m-w[i]] = c[i-1][m-w[i]],需要逆序,确保当前i的情况下,比m小的值还没有计算出来。

    完全背包

    这个问题非常类似于01背包问题,所不同的是每种物品有无限件,也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……取M/w[i]件等很多种。如果仍然按照解01背包时的思路,令c[i][m]表示前i种物品放入一个容量为m的背包的最大价值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

    c[i][m]=max{c[i-1][m],c[i-1][m-k*w[i]]+k*p[i]}(0<=k*w[i]<=m)

    这跟01背包问题一样有O(N*M)个状态需要求解,但求解每个状态c[i][m]的时间是O(m/w[i]),总的复杂度是超过O(NM)的。

    时间复杂度优化为O(NM) (精华)

    将原始算法的思想转变一下。

    设c[i][m]表示在前i种物品中选取若干件物品放入容量为m的背包所得的最大价值。那么对于第i种物品的出现,我们对第i种物品放不放入背包进行决策。如果不放那么c[i][m]=c[i-1][m];如果确定放,背包中应该出现至少一件第i种物品,所以c[i][m]中至少应该出现一件第i种物品,即c[i][m]=c[i][m-w[i]]+p[i]。为什么会是c[i][m-w[i]]+p[i]? p[i]是当前选择放i这种物品以后获得的价值,既然选择了放一次i,那么对应的重量就要减去w[i],对于剩下的重量m-w[i],由于是完全背包,我们仍然有i种物品可放,c[i][m-w[i]]即表示在重量为m-w[i]的情况下,i种物品能获得的最大值。这个最大值加上p[i]的结果正是我们要来跟c[i-1][m]比较的。

    状态方程为:

                    

    思想改变后,就省去了对于不同的k值情况的循环。但是为什么结果就能跟上面的老公式一样呢?还是会存有疑惑。

    让我们来再详细分析一下,旧的公式,对于第i种物品,我们需要比较c[i-1][m],c[i-1][m-w[i]]+p[i],c[i-1][m-2*w[i]]+2*p[i],c[i-1][m-3*w[i]]+3*p[i] ....等等的最大值,新的公式c[i-1][m]不变,那就来看为什么c[i][m-w[i]]+p[i] 可以表示c[i-1][m-w[i]]+p[i],c[i-1][m-2*w[i]]+2*p[i],c[i-1][m-3*w[i]]+3*p[i] ....等等的最大值?

    从新公式,我们知道c[i][m-w[i]]来源于max{c[i-1][m-w[i]], c[i][m-w[i]-w[i]] + p[i]}, 而同样考虑下去,c[i][m-w[i]-w[i]] 呢?c[i][m-w[i]-w[i]]  又来源于max{c[i-1][m-w[i]-w[i]], c[i][m-w[i]-w[i]-w[i]] + p[i]}.

    聪明的童鞋应该可以发现了吧。

    c[i][m-w[i]]+p[i]

    = max{ c[i-1][m-w[i]], c[i][m-w[i]-w[i]] + p[i] }+p[i]

    = max{ c[i-1][m-w[i]]+p[i], c[i][m-w[i]-w[i]] + p[i]+p[i] }

    = max{  c[i-1][m-w[i]]+p[i], max{ c[i-1][m-w[i]-w[i]], c[i][m-w[i]-w[i]-w[i]] + p[i] } + p[i]+p[i]  }

    = max{  c[i-1][m-w[i]]+p[i], max{ c[i-1][m-w[i]-w[i]]+ p[i]+p[i], c[i][m-w[i]-w[i]-w[i]] + p[i]+ p[i]+p[i] }    } 

    = max{  c[i-1][m-w[i]]+p[i], max{ c[i-1][m-2*w[i]]+ 2*p[i], c[i][m-3*w[i]] + 3*p[i] }  } 

    当然你还可以继续推导,但现在已经可以看出来了吧,c[i-1][m-w[i]]+p[i],c[i-1][m-2*w[i]]+2*p[i],c[i-1][m-3*w[i]]+3*p[i] ....这一系列状态在我们求解c[i][m-w[i]]+p[i]的时候都已经被比较过了,c[i][m-w[i]]+p[i]就是这些状态的最大值。

    修改公式后的代码如下

    #include <iostream>
    
    using namespace std;
    
    const int MAXN = 100;
    const int MAXM = 1000;
    
    
    /**
     * 完全背包
     * @param n 物品个数
     * @param m 背包承重量
     * @param p[] 物品价值
     * @param w[] 物品重量
     * @return 最大价值
     */
    int entirelyKnapsack(int n, int m, int p[], int w[])
    {
        int c[MAXN][MAXM];
        for(int i=1; i<=n; ++i)
        {
            c[i][0] = 0;
        }
    
        for(int i=1; i<=m; ++i)
        {
            c[0][i] = 0;
        }
    
        for(int i=1; i<=n; ++i)
        {
            for(int j=1; j<=m; ++j)
            {
    
                if(w[i]<=j)
                {
                    
                    if(c[i-1][j] <  c[i][j-w[i]]+p[i])
                    {
                        c[i][j] = c[i][j-w[i]]+p[i];
                    }
                    else
                    {
                        c[i][j] = c[i-1][j];
                    }
    
                }
                else
                {
                    c[i][j] = c[i-1][j];
                }
            }
        }
    
        return c[n][m];
    }
    
    
    int main(int argc, char** argv)
    {
    
        int p[MAXN];
        int w[MAXM];
        p[1] = 4;
        p[2] = 5;
        p[3] = 6;
        w[1] = 3;
        w[2] = 4;
        w[3] = 5;
    
        //cout << knapsack01(3,10,p,w) <<endl;
    
        cout << entirelyKnapsack(3,10,p,w) <<endl;
    
        return 0;
    }

    最后我们再来讨论一下完全背包的一维数组优化

    和01背包问题一样,完全背包也可以用一维数组f[0..m]来保存数据。算法样式和01背包的很相似,唯一不同的是对m遍历时变为正序,而01背包为逆序。

    为什么又是正序了呢??

    比较一下01背包和完全背包的公式

    01背包    c[i][m]=max{c[i-1][m],c[i-1][m-w[i]]+p[i]}

    完全背包  c[i][m]=max{c[i-1][m],c[i][m-w[i]]+p[i]}

    区别就在这里,01背包逆序是想用f[m-w[i]]+p[i] 表示 c[i-1][m-w[i]]+p[i],确保在考虑f[m-w[i]]的时候,c[i][m-w[i]]还没有被计算出来存入f[m-w[i]]而完全背包呢,f[m-w[i]]+p[i] 应该表示的就是 c[i][m-w[i]]+p[i],我们需要考虑重量减少但还可以放i这种物品的情况,所以使用了正序。

    for i=1..N
    
       for m=0...M
    
            f[m]=max{f[m],f[m-w[i]]+p[i]};

     

    其他

    将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的状态转移方程可以推及其它类型的背包问题。但是由于复杂度太高,我们还是试图改进这个复杂度。

    完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足p[i]<=p[j]且w[i]>=w[j],则将物品i去掉,不用考虑。这个优化的正确性显然:任何情况下都可将重量大费用高得i换成物美价廉的j,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。这个优化可以简单的O(N^2)地实现,一般都可以承受。

    另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于M的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(M+N)地完成这个优化。

    既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选M/w[i]件,于是可以把第i种物品转化为M/w[i]件价值为p[i]及重量为w[i]的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。

     

  • 相关阅读:
    markdown语法
    GIT基本操作
    函数rest参数和扩展
    axios基础介绍
    Vue-Resource的使用
    Vue-router的介绍
    Vue2.0+组件库总结
    Vue 项目de一些准备工作
    VUE.js入门学习(5)- 插槽和作用域插槽
    VUE.js入门学习(4)-动画特效
  • 原文地址:https://www.cnblogs.com/scarecrow-blog/p/3736624.html
Copyright © 2020-2023  润新知