何时使用自下而上的DP以及何时使用自上而下的DP

时间:2016-01-20 10:28:32

标签: algorithm dynamic-programming

我已经尝试了两种DP方式,但我现在很困惑。我们如何选择不同的条件?而且我发现在大多数情况下,自上而下对我来说更自然。任何人都可以告诉我如何做出选择。

PS:我看过这篇文章older post,但仍然感到困惑。需要帮忙。不要将我的问题标识为重复。我已经提到它们是不同的。我希望知道如何选择以及何时从上到下或自下而上考虑问题。

4 个答案:

答案 0 :(得分:1)

为简单起见,我将根据一些sources

的摘要进行解释
  1. 自上而下:看起来像是:a(n) = a(n-1) + a(n-2)。使用此等式,您可以通过使函数a调用自身来实现大约4-5行代码。正如您所说,它的优势对于大多数开发人员来说非常直观,但它需要花费更多的空间(内存堆栈)来执行。
  2. 自下而上:您计算a(0)然后a(1),并将其保存到某个数组(例如),然后您不断保存a(i) = a(i-1) + a(i-2)。使用此方法,您可以显着提高代码的性能。使用大n,可以避免堆栈溢出。

答案 1 :(得分:0)

如果您喜欢自上而下的自然,那么如果您知道可以实施它,请使用它。自下而上比自上而下更快。有时自下而上更容易,而且大多数情况下自下而上都很容易。根据您的情况做出决定。

答案 2 :(得分:0)

自下而上和自上而下的DP方法在时间和空间复杂性方面对于许多问题是相同的。不同之处在于,自下而上快一点,因为你不需要增加递归的开销,是的,自上而下更直观和自然。

但是,上下方法的真正优势可能在于一些小任务,您不需要为所有较小的子任务计算答案!在这种情况下,您可以减少时间复杂度。

例如,您可以使用自上而下的记忆方法来查找第N个斐波纳契数,其中序列定义为[n] = a [n-1] + a [n-2]所以,你有O(N)计算它的时间(我不能与O(logN)解决方案进行比较以找到这个数字)。但是看一下a [n] = a [n / 2] + a [n / 2-1]的序列,其中有一些小N的边缘情况。在botton up方法中你不能比O(N更快)做到这一点。 )自上而下的算法将使用复杂度O(logN)(或者可能是一些多对数复杂度,我不确定)

答案 3 :(得分:0)

答案稍长一些,但是我试图解释我自己的动态编程方法以及解决这些问题后所学到的东西。希望以后的用户对它有所帮助。请随时发表评论和讨论:

考虑动态编程问题时,自上而下的解决方案会更加自然。您从最终结果开始,然后尝试弄清楚到达那里的方式。例如,对于fib(n),我们知道只能通过fib(n-1)和fib(n-2)才能到达此处。因此,我们再次递归调用函数以计算这两种情况的答案,这两种情况会越来越深地进入树中,直到达到基本情况为止。然后将答案建立起来,直到弹出所有堆栈,我们得到最终结果。

为了减少重复的计算,我们使用了一个缓存,该缓存存储一个新结果,并在函数尝试再次计算时将其返回。因此,如果您想象一棵树,则函数调用不必一直走到叶子,它已经有了答案,因此可以返回它。这称为记忆,通常与自顶向下方法相关。

现在,对于自下而上的方法,我认为重要的一点是,您必须知道最终解决方案的构建顺序。在自上而下的情况下,您只是将一件事分解为许多,但在自下而上的情况下,您必须知道计算中涉及的状态的数量和顺序,以便从一个级别转到另一个级别。在一些较简单的问题(例如fib(n))中,这很容易看到,但在更复杂的情况下,它并不自然地适合自己。我通常采用的方法是自上而下思考,将最终案例分解为先前的状态,并尝试找到一种模式或顺序,然后能够对其进行备份。

关于何时选择这些状态中的任何一个,我建议采用上述方法来确定状态如何相互关联并建立状态。您可以找到这种方式的一个重要区别是,实际上需要进行多少次计算,而其中多少可能只是多余的。在自下而上的情况下,您必须先填充整个级别,然后再进行下一个级别。但是,在自上而下的情况下,如果不需要,可以跳过整个子树,这样可以节省很多额外的计算。

因此,选择显然取决于问题,而且取决于状态之间的相互关系。通常建议使用自底向上的方法,因为与递归方法相比,它可以节省堆栈空间。但是,如果您觉得递归不是很深,而是非常广泛,并且可以通过表格化来进行大量不必要的计算,则可以采用自顶向下的方式进行记忆化。

例如,在以下问题中:https://leetcode.com/problems/partition-equal-subset-sum/,如果您看到讨论,则提到自顶向下的速度比自底向上的速度快,基本上,具有缓存的二叉树方法与自上而下的背包建立。我将其作为练习来了解状态之间的关系。