vlambda博客
学习文章列表

背包问题——是动态规划还是贪心算法?

     今天,我们将从一个经典最优化问题的两个变形:0-1背包部分背包问题入手,“初识”动态规划和贪心算法,比一比谁更胜一筹。
背包问题——是动态规划还是贪心算法?
小迷糊

这一篇我们先来详细说一说,什么是0-1背包问题?


背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

当我们需要解决一个现实生活中的实际问题时,首先,能够对其进行正确地形式化定义是求解问题的前提。(都怪小迷糊当初语文没有学好背包问题——是动态规划还是贪心算法?

背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

如果我们当前想不出好的解决办法怎么办,不如试试蛮力枚举法(反正小迷糊多的是背包问题——是动态规划还是贪心算法?),说不定还可以找到新的灵感,千万不要放弃呀!背包问题——是动态规划还是贪心算法?


背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

然后,检查体积约束,筛除掉超过背包体积(13)的商品组合。


背包问题——是动态规划还是贪心算法?

背包问题——是动态规划还是贪心算法?
小迷糊

最后,我们选取总价值最大的背包


背包问题——是动态规划还是贪心算法?

背包问题——是动态规划还是贪心算法?
小迷糊

下面我们用计算机语言来描述上述求解过程,函数KnapsackSR(h,i,c)表示在第h个商品到第i个商品中,容量为c时的最优解。


背包问题——是动态规划还是贪心算法?



这样,我们可以得到递归的一般式为:KnapsackSR(h,i,c)=max{KnapsackSR(h,i-1,c-vi)+pi, KnapsackSR(h,i-1,c)}。
它的含义:在第h个商品到第i个商品中,容量为c时的最优解是子问题(从第h个商品到第i-1个商品中的最优解)得到的。若我们已知子问题的最优解,原问题的最优解可以选择在子问题满足体积约束条件c的前提下 添加商品pi 或者不添加商品pi 由两者之中的 (函数KnapsackSR返回值) 最大值得到。
背包问题——是动态规划还是贪心算法?
小迷糊

下面来看看它的伪代码

背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

接下来,我们用之前学过的来分析蛮力枚举算法的时间复杂度。


背包问题——是动态规划还是贪心算法?

     我发现在递归求解的过程中存在着大量需要重复计算的子问题,如果我们对这些重复计算的子问题进行优化,将递归求解的子问题用备忘录P[i,c](数组)记录下来,每次从备忘录P[i,c]中读取子问题的最优解,能否降低它的时间复杂度呢?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

下面是它的伪代码。

背包问题——是动态规划还是贪心算法?
     我们可以看到,我们每一次将子问题的最优解记录下来,当遇到重复计算的问题(备忘录不为空)时,可以直接返回备忘录中的最优解,这个过程即动态规划中的一种等价的实现方法——带备忘的自顶向下法。这种方法仍按自然的递归形式实现算法,但会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常的方式计算这个子问题。我们称这个递归过程是带备忘的,因为它“记住了”之前已经计算出的结果。



接下来,我们分析本算法的求解过程:
  • 刻画最优解结构(如果一个问题的最优解包含其子问题的最优解,我们称此问题具有最优子结构的性质);

  • 自顶向下的递归定义最优解的值。


背包问题——是动态规划还是贪心算法?


     那么我们能否对上述算法继续优化呢?是否可以不递归(只要自底向上的计算)就可以直接求解P[i,c]
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

如果我们将每个子问题的最优解P[i,c]存在一个表中,观察子问题的依赖关系,可以发现P[i,c]由其左上方的子问题P[i-1,c-vi]和上方的子问题P[i-1,c]计算而得。


背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

如果我们不用递归算法,则需要找到子问题的计算顺序(先求较小的子问题,然后是较大的子问题),你能填满这张表吗?


背包问题——是动态规划还是贪心算法?

     当我们计算得到了全局最优解,又该如何确定选取了哪些商品呢?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

那我们就需要另一张表来记录这个决策过程,这个过程就是最优解的追踪


背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

最后,追踪最优解,即回溯的过程——倒序判断是否选择商品,根据选择结果,确定最优子问题。


背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
     上述求解的过程就是动态规划的第二种等价的实现方法——自底向上法。这种方法一般需要恰当定义子问题“规模”,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因而,我们可以将子问题按规模排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存在表中。每个子问题只需求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完毕。



这种自底向上的动态规划算法的求解步骤为:
  • 刻画最优解结构(如果一个问题的最优解包含其子问题的最优解,我们称此问题具有最优子结构的性质);

  • 自顶向下的递归定义最优解的值;

  • 采用自底向上的方法计算最优解的值;

  • 最优方案追踪。



      无论是带备忘的自顶向下法,还是自底向上法,这两种动态规划方法具有相同的渐近运行时间,仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂度函数通常具有更小的系数。



下面我们来回顾一下0-1背包问题动态规划求解的一般步骤:
1. 问题结构分析
    a)  给出问题表示
        l  P[i,c]:前i个商品可选、背包容量为c时的最大总价格。
    b)明确原始问题
        l  P[n,C]:前n个商品可选、背包容量为C时的最大总价格。
2. 递推关系建立
    a)分析最优(子)结构
        l  问题的最优解由相关子问题最优解组合而成,子问题可以独立求解。
背包问题——是动态规划还是贪心算法?
    b)构造递推公式
背包问题——是动态规划还是贪心算法?
3. 自底向上计算
    a)确定计算顺序
    b)依次求解问题
背包问题——是动态规划还是贪心算法?
4. 最优方案追踪
    a)记录决策过程
背包问题——是动态规划还是贪心算法?
    b)输出最优方案
背包问题——是动态规划还是贪心算法?

背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
本篇小结
动态规划四个步骤

  • 问题结构分析:给出问题表示,明确原始问题
  • 递推关系建立:分析最优结构,构造递推公式

  • 自底向上计算:确定计算顺序,依次求解问题

  • 最优方案追踪:记录决策过程,输出最优方案



      可是,我发现动态规划算法的求解步骤和我们之前学过的感觉好像哦:都是通过组合子问题的解来求解原问题,对吗?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
背包问题——是动态规划还是贪心算法?
小迷糊

没错,动态规划和分治算法的主要区别就在于子问题的划分

     

子问题

重复子问题

子问题求解

动态规划

互相重叠

只求解一次(保存在表格或备忘录中)

既可以是带备忘的递归方法,也可以是自底向上的表格法

分治算法

互不相交

多次求解

递归


     下一篇我们继续学习, 为什么部分背包问题可以用贪心算法求解呢?