0-1背包问题

    0-1背包问题是经典的动态规划的例子,个人觉得这是个非常好的问题,可以用许多方法来解决,而且思路也各有不同。

    个人比较推荐这三个博客:http://love-oriented.com/pack/, http://www.wutianqi.com/?p=539, http://www.cnblogs.com/FreeAquar/archive/2011/12/17/2291436.html,将动态规划问题讲解得比较详细。但是里面也难免有些地方不是很清楚。这里我也讲讲我的看法,顺便对第二篇文章做点补充。

    先定义0-1背包问题,注意这里的符号与下面解题的前后一致:

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

    来一个例子:

    有3件物品,背包容量为10,物品重量的数组为w={3,4,5}, 价值的数组为v={4,5,6}

    例子的好处是可以随时验证我们的想法是否正确

    如果一开始不告诉你如何来解0-1背包问题,第一个冒出来的解题思路是什么?

    尽可能的放价值大而重量小的物品,按照r[i]=v[i]/w[i]从大到小装物品。等等,用例子验证一下,这里r={4/3,5/4,6/5},也就是应该先装第一个,再装第二个,第三个装不下了,于是,得到的最大价值为4+5=9。……直观上看去,5+6=11才是正解,那么,这种方式无法解决这个问题。好吧,我承认,刚刚耍了你,只不过这个是思维的一种方式。这种方式称为贪心法贪心法适合解决分数背包问题(即物品可以切割),适用贪心法解决的问题应当有最优子结构以及贪心选择性质。所谓的贪心选择性质,就是每一步都只需要考虑当前问题,而不需要考虑子问题(所以刚刚的想法是第一个冒出来的,因为非常简单,每次都只需要考虑当前的价值/重量最大的物品)。

    接下来想的可能就是最简单暴力能够实现的方式,即穷举法一个一个列举所有能进入背包的可能性,将只取1个,到只取2个,到只取n个的所有可能算出来,由于这里3个会撑爆背包,所以下图未列出3个的组合,计算完毕之后按照价值倒序排列就是想要的结果。可以看到,这里的计算次数是c(3,1) + c(3,2),写成一般形式就是c(n,1)+c(n,2)+...+c(n,n),而这个

结果是1+2+2²+……2^n-1,虽然有可能最后一些结果可以被剪掉(比如这里的3-3),但是可以想象,当n增大时无论时间空间都是不允许的。

    

    好吧,想想看这里有什么问题?

    这个时候,应当想到,如何将原问题分解为子问题,这是一种比较常用的思想,运用这种思想的两大类算法就是分治法和动态规划,因此非常的牛X。可以想到的一种分解子问题的方式是:使用当前物品后重量为W的解空间不使用当前物品重量就为W的解空间中较大的一个。于是,依据这种子结构,可以将问题用递归实现出来,这里简单写了一个。

public class Pack {

        private static int packDp(int start, int maxW, int wei, int[] weight,
                      int[] value) {
               if (start >= weight.length )
                      return 0;
               int nextStart = start + 1;
               int valueIn = value[start]
                           + packDp(nextStart, maxW, wei + weight[start], weight,
                                         value);
               int valueNotIn = packDp(nextStart, maxW, wei, weight, value);
               if(valueIn > valueNotIn){
                      if(maxW < wei + weight[start]){
                            return 0;
                     } else{
                            return valueIn;
                     }
              } else{
                      if (maxW < wei) {
                            return 0;
                     } else{
                            return valueNotIn;
                     }
              }
       }

        public static void main(String[] args) {
               int[] weight = { 3, 4, 5 };
               int[] value = { 4, 5, 6};
               int maxW = 10;
              System. out.println(packDp(0, maxW, 0, weight, value));
       }

}

    可以将这个递归过程用图简单画出来,以上面例子为例,最初是求1,2,3这个问题域,但是第一次划分就将问题化为两个子问题,左分支是物品1在解里达到10的重量的情况,右分支是物品1不在解里面达到10的重量的情况,这两种情况取大者就是问题的实际价值。线段上的1表示物品编号,3表示物品1的重量,7表示除去物品1当前剩余重量。而左分支选择以后的实际价值就变成物品1的价值+packDp(2,3)这个子问题的价值,即4+packDp(2,3)。


    仔细分析一下,上面的解法有什么问题?似乎求解的时候重复了一些子问题,也就是说子问题重叠了(使用递归求解斐波那契数列时这个问题特别明显)。

    使用动态规划的两个特征就是最优子结构以及子问题重叠。而动态规划的精髓就在于使用了额外的存储保存了更小的子问题。这种保存有两种方式。

    一是自顶向下,典型的就如上面的递归,如果要使用递归来实现同样的问题,则必须使用备忘机制来保存中间的子问题结果(例如一个hashtable),以便下次计算时直接取得。

    二是自底向上,典型的方式就是迭代,迭代通常使用数组一步一步的向上保存子问题的解,上层每一步都会使用已经计算出来的子问题解。

    下面是动态规划迭代法实现的过程,f[r]表示将前i件物品放入容量为w的背包时的价值,而到底放入第几件是由循环次数i来决定的。这里,我也推荐手动计算一下以便理解这个过程。有不理解可以参考第二篇博客

for i=1..N
   for r=W..0
        f[r]=max{f[r],f[r-w[i]]+v[i]};

   

    实际上,后面还可以用回溯法跟分支限界法来解决,可以参考推荐的第三篇博客。


    最后了,想起一个比较有趣的题目:有n级台阶,每次可以跳1级,2级……直到n级,那么跳这n级台阶一共有几种跳法?

    这个题比较有趣,借助于这个问题,可以帮我们梳理下动态规划法的精髓。


    参考:

http://love-oriented.com/pack/

        http://www.wutianqi.com/?p=539

        http://www.cnblogs.com/FreeAquar/archive/2011/12/17/2291436.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值