自下而上和自上而下有什么区别?

发布于 2024-11-10 07:01:15 字数 162 浏览 0 评论 0原文

自下而上方法(动态规划)首先查看“较小”的子问题,然后使用较小问题的解决方案来解决较大的子问题。

自上而下在于以“自然的方式”解决问题,并检查您之前是否计算过子问题的解决方案。

我有点困惑。这两者有什么区别?

The bottom-up approach (to dynamic programming) consists in first looking at the "smaller" subproblems, and then solve the larger subproblems using the solution to the smaller problems.

The top-down consists in solving the problem in a "natural manner" and check if you have calculated the solution to the subproblem before.

I'm a little confused. What is the difference between these two?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

惟欲睡 2024-11-17 07:01:15

rev4:用户 Smaron 的一个非常雄辩的评论指出,也许这个答案之前混淆了自上而下和自下而上。虽然最初这个答案(rev3)和其他答案说“自下而上是记忆”(“假设子问题”),但它可能是相反的(也就是说,“自上而下”可能是“假设子问题”和“自下而上”可能是“组成子问题”)。以前,我读过记忆化是一种不同类型的动态编程,而不是动态编程的子类型。尽管我没有同意,但还是引用了这个观点。我已经重写了这个答案,以不了解术语,直到在文献中找到正确的参考文献。我还将此答案转换为社区维基。请优先选择学术来源。参考文献列表:{Web:12} {文献:5}

回顾

动态规划就是在一个避免重新计算重复工作的方式。您有一个主要问题(子问题树的根)和子问题(子树)。 子问题通常会重复并重叠

例如,考虑一下您最喜欢的斐波那契示例。如果我们进行简单的递归调用,这就是完整的子问题树:(

TOP of the tree
fib(4)
 fib(3)...................... + fib(2)
  fib(2)......... + fib(1)       fib(1)........... + fib(0)
   fib(1) + fib(0)   fib(1)       fib(1)              fib(0)
    fib(1)   fib(0)
BOTTOM of the tree

在其他一些罕见问题中,这棵树在某些分支中可能是无限的,代表非终止,因此树的底部可能无限大。此外,在某些问题中,您可能事先不知道完整的树是什么样子,因此,您可能需要一种策略/算法来决定要揭示哪些子问题。)


记忆化、制表

动态规划至少有两种主要技术:不互斥:

  • 记忆 - 这是一种自由放任的方法:您假设您已经计算了所有子问题,并且您不知道最佳评估顺序是什么。通常,您会从根执行递归调用(或某些迭代等效项),并且希望您将接近最佳评估顺序,或者获得将帮助您达到最佳评估顺序的证明。您将确保递归调用永远不会重新计算子问题,因为您缓存结果,因此不会重新计算重复的子树。

    • 示例:如果您要计算斐波那契数列 fib(100),您只需调用此函数,它就会调用 fib(100)=fib (99)+fib(98),这将调用 fib(99)=fib(98)+fib(97),...等等...,这将调用 <代码>fib(2)=fib(1)+fib(0)=1+0=1。然后它最终会解析fib(3)=fib(2)+fib(1),但不需要重新计算fib(2),因为我们缓存了它。
    • 这从树的顶部开始,评估从叶/子树到根的子问题。
  • 制表 - 您也可以将动态规划视为“表格填充”算法(尽管通常是多维的,但该“表格”在极少数情况下可能具有非欧几里得几何*)。这类似于记忆,但更活跃,并且涉及一个额外的步骤:您必须提前选择进行计算的确切顺序。这并不意味着顺序必须是静态的,而是意味着您比记忆具有更大的灵活性。

    • 示例如果您正在执行斐波那契计算,您可以选择按以下顺序计算数字:fib(2),fib(3),fib(4)... 缓存每个值,以便您可以更轻松地计算下一个值。您也可以将其视为填充表格(另一种形式的缓存)。
    • 我个人很少听到“制表”这个词,但这是一个非常合适的术语。有些人认为这是“动态规划”。
    • 在运行算法之前,程序员会考虑整个树,然后编写一个算法来按特定顺序向根评估子问题,通常会填写一个表格。
    • *脚注:有时“表格”本身并不是具有网格状连接的矩形表格。相反,它可能具有更复杂的结构,例如树,或特定于问题域的结构(例如地图上飞行距离内的城市),甚至是网格图,虽然网格状,但没有上-下-左-右连接结构等。例如,user3290797 链接了一个动态编程示例,该示例查找 一棵树中的最大独立集,对应于树中的填空。

(最一般地说,在“动态编程”范例中,我想说程序员考虑整个树,然后编写一个算法来实现评估子问题的策略,该策略可以优化您想要的任何属性(通常是时间复杂性和空间复杂性的组合)。您的策略必须从某个特定的子问题开始,并且可能会根据这些评估的结果进行自我调整。在“动态规划”的一般意义上,您可以尝试。缓存这些子问题,更一般地说,尽量避免重新审视具有细微区别的子问题,这些子问题可能是各种数据结构中的图形的情况。通常,这些数据结构就像数组或表一样是其核心,子问题的解决方案可以被丢弃。如果我们不再需要它们了。)

[之前,这个答案对自上而下与自下而上的术语进行了陈述;显然有两种主要方法称为“记忆化”和“制表法”,它们可能与这些术语存在双射(尽管不完全)。大多数人使用的通用术语仍然是“动态编程”,有些人说“记忆化”来指代“动态编程”的特定子类型。在社区能够在学术论文中找到适当的参考文献之前,这个答案拒绝说明哪个是自上而下的,哪个是自下而上的。最终,重要的是理解区别而不是术语。]


优点和缺点

易于编码

记忆化非常容易编码(您通常可以*编写一个“memoizer”注释或包装函数来自动为您执行此操作),并且应该是你的第一线方法。制表的缺点是您必须提出排序。

*(实际上,只有当您自己编写函数和/或使用不纯/非函数式编程语言进行编码时,这才容易......例如,如果有人已经编写了预编译的 fib 函数,那么它必然会对其自身进行递归调用,并且如果不确保这些递归调用调用新的记忆函数(而不是原始的未记忆函数),您就无法神奇地记忆该函数)

递归性

请注意,自上而下和自下而上都可以通过以下方式实现递归或迭代表填充,尽管它可能不自然。

实际问题

对于记忆化,如果树很深(例如fib(10^6)),您将耗尽堆栈空间,因为每个延迟计算都必须放在堆栈上,并且您将有 10^6 个。

最优性

如果您发生(或尝试)访问子问题的顺序不是最优的,特别是如果有不止一种方法来计算子问题(通常缓存可以解决这个问题,但理论上缓存是可能的),那么这两种方法都可能不是时间最优的在某些特殊情况下可能不会)。记忆通常会增加你的时间复杂度和空间复杂度(例如,使用制表你可以更自由地放弃计算,就像使用带有 Fib 的制表可以让你使用 O(1) 空间,但是带有 Fib 的记忆使用 O(N)堆栈空间)。

高级优化

如果您也在做一个极其复杂的问题,您可能别无选择,只能进行制表(或者至少在引导记忆到您想要的地方方面发挥更积极的作用)。此外,如果您处于优化绝对至关重要并且必须优化的情况,制表将允许您进行优化,而记忆则不允许您以理智的方式进行优化。以我的拙见,在正常的软件工程中,这两种情况都不会出现,所以我只会使用记忆化(“缓存其答案的函数”),除非某些东西(例如堆栈空间)使制表成为必要......从技术上讲,为了避免堆栈溢出,您可以1)在允许的语言中增加堆栈大小限制,或2)消耗一定的额外工作来虚拟化堆栈(ick),或3)以连续传递方式进行程序,这实际上还虚拟化了你的堆栈(不确定这的复杂性,但基本上你将有效地从大小为 N 的堆栈中获取延迟调用链,并事实上将其粘贴在 N 个连续嵌套的 thunk 函数中......尽管在某些语言中没有尾部调用优化(您可能必须对事物进行蹦床以避免堆栈溢出)。


更复杂的例子

这里我们列出了特别有趣的例子,它们不仅仅是一般的 DP 问题,而且有趣地区分了记忆化和制表。例如,一种公式可能比另一种公式容易得多,或者可能有一种基本上需要制表的优化:

  • 计算编辑距离的算法[4],作为二维表格填充算法的重要示例,很有趣

rev4: A very eloquent comment by user Sammaron has noted that, perhaps, this answer previously confused top-down and bottom-up. While originally this answer (rev3) and other answers said that "bottom-up is memoization" ("assume the subproblems"), it may be the inverse (that is, "top-down" may be "assume the subproblems" and "bottom-up" may be "compose the subproblems"). Previously, I have read on memoization being a different kind of dynamic programming as opposed to a subtype of dynamic programming. I was quoting that viewpoint despite not subscribing to it. I have rewritten this answer to be agnostic of the terminology until proper references can be found in the literature. I have also converted this answer to a community wiki. Please prefer academic sources. List of references: {Web: 1,2} {Literature: 5}

Recap

Dynamic programming is all about ordering your computations in a way that avoids recalculating duplicate work. You have a main problem (the root of your tree of subproblems), and subproblems (subtrees). The subproblems typically repeat and overlap.

For example, consider your favorite example of Fibonnaci. This is the full tree of subproblems, if we did a naive recursive call:

TOP of the tree
fib(4)
 fib(3)...................... + fib(2)
  fib(2)......... + fib(1)       fib(1)........... + fib(0)
   fib(1) + fib(0)   fib(1)       fib(1)              fib(0)
    fib(1)   fib(0)
BOTTOM of the tree

(In some other rare problems, this tree could be infinite in some branches, representing non-termination, and thus the bottom of the tree may be infinitely large. Furthermore, in some problems you might not know what the full tree looks like ahead of time. Thus, you might need a strategy/algorithm to decide which subproblems to reveal.)


Memoization, Tabulation

There are at least two main techniques of dynamic programming which are not mutually exclusive:

  • Memoization - This is a laissez-faire approach: You assume that you have already computed all subproblems and that you have no idea what the optimal evaluation order is. Typically, you would perform a recursive call (or some iterative equivalent) from the root, and either hope you will get close to the optimal evaluation order, or obtain a proof that you will help you arrive at the optimal evaluation order. You would ensure that the recursive call never recomputes a subproblem because you cache the results, and thus duplicate sub-trees are not recomputed.

    • example: If you are calculating the Fibonacci sequence fib(100), you would just call this, and it would call fib(100)=fib(99)+fib(98), which would call fib(99)=fib(98)+fib(97), ...etc..., which would call fib(2)=fib(1)+fib(0)=1+0=1. Then it would finally resolve fib(3)=fib(2)+fib(1), but it doesn't need to recalculate fib(2), because we cached it.
    • This starts at the top of the tree and evaluates the subproblems from the leaves/subtrees back up towards the root.
  • Tabulation - You can also think of dynamic programming as a "table-filling" algorithm (though usually multidimensional, this 'table' may have non-Euclidean geometry in very rare cases*). This is like memoization but more active, and involves one additional step: You must pick, ahead of time, the exact order in which you will do your computations. This should not imply that the order must be static, but that you have much more flexibility than memoization.

    • example: If you are performing fibonacci, you might choose to calculate the numbers in this order: fib(2),fib(3),fib(4)... caching every value so you can compute the next ones more easily. You can also think of it as filling up a table (another form of caching).
    • I personally do not hear the word 'tabulation' a lot, but it's a very decent term. Some people consider this "dynamic programming".
    • Before running the algorithm, the programmer considers the whole tree, then writes an algorithm to evaluate the subproblems in a particular order towards the root, generally filling in a table.
    • *footnote: Sometimes the 'table' is not a rectangular table with grid-like connectivity, per se. Rather, it may have a more complicated structure, such as a tree, or a structure specific to the problem domain (e.g. cities within flying distance on a map), or even a trellis diagram, which, while grid-like, does not have a up-down-left-right connectivity structure, etc. For example, user3290797 linked a dynamic programming example of finding the maximum independent set in a tree, which corresponds to filling in the blanks in a tree.

(At it's most general, in a "dynamic programming" paradigm, I would say the programmer considers the whole tree, then writes an algorithm that implements a strategy for evaluating subproblems which can optimize whatever properties you want (usually a combination of time-complexity and space-complexity). Your strategy must start somewhere, with some particular subproblem, and perhaps may adapt itself based on the results of those evaluations. In the general sense of "dynamic programming", you might try to cache these subproblems, and more generally, try avoid revisiting subproblems with a subtle distinction perhaps being the case of graphs in various data structures. Very often, these data structures are at their core like arrays or tables. Solutions to subproblems can be thrown away if we don't need them anymore.)

[Previously, this answer made a statement about the top-down vs bottom-up terminology; there are clearly two main approaches called Memoization and Tabulation that may be in bijection with those terms (though not entirely). The general term most people use is still "Dynamic Programming" and some people say "Memoization" to refer to that particular subtype of "Dynamic Programming." This answer declines to say which is top-down and bottom-up until the community can find proper references in academic papers. Ultimately, it is important to understand the distinction rather than the terminology.]


Pros and cons

Ease of coding

Memoization is very easy to code (you can generally* write a "memoizer" annotation or wrapper function that automatically does it for you), and should be your first line of approach. The downside of tabulation is that you have to come up with an ordering.

*(this is actually only easy if you are writing the function yourself, and/or coding in an impure/non-functional programming language... for example if someone already wrote a precompiled fib function, it necessarily makes recursive calls to itself, and you can't magically memoize the function without ensuring those recursive calls call your new memoized function (and not the original unmemoized function))

Recursiveness

Note that both top-down and bottom-up can be implemented with recursion or iterative table-filling, though it may not be natural.

Practical concerns

With memoization, if the tree is very deep (e.g. fib(10^6)), you will run out of stack space, because each delayed computation must be put on the stack, and you will have 10^6 of them.

Optimality

Either approach may not be time-optimal if the order you happen (or try to) visit subproblems is not optimal, specifically if there is more than one way to calculate a subproblem (normally caching would resolve this, but it's theoretically possible that caching might not in some exotic cases). Memoization will usually add on your time-complexity to your space-complexity (e.g. with tabulation you have more liberty to throw away calculations, like using tabulation with Fib lets you use O(1) space, but memoization with Fib uses O(N) stack space).

Advanced optimizations

If you are also doing a extremely complicated problems, you might have no choice but to do tabulation (or at least take a more active role in steering the memoization where you want it to go). Also if you are in a situation where optimization is absolutely critical and you must optimize, tabulation will allow you to do optimizations which memoization would not otherwise let you do in a sane way. In my humble opinion, in normal software engineering, neither of these two cases ever come up, so I would just use memoization ("a function which caches its answers") unless something (such as stack space) makes tabulation necessary... though technically to avoid a stack blowout you can 1) increase the stack size limit in languages which allow it, or 2) eat a constant factor of extra work to virtualize your stack (ick), or 3) program in continuation-passing style, which in effect also virtualizes your stack (not sure the complexity of this, but basically you will effectively take the deferred call chain from the stack of size N and de-facto stick it in N successively nested thunk functions... though in some languages without tail-call optimization you may have to trampoline things to avoid a stack blowout).


More complicated examples

Here we list examples of particular interest, that are not just general DP problems, but interestingly distinguish memoization and tabulation. For example, one formulation might be much easier than the other, or there may be an optimization which basically requires tabulation:

  • the algorithm to calculate edit-distance[4], interesting as a non-trivial example of a two-dimensional table-filling algorithm
不即不离 2024-11-17 07:01:15

自上而下和自下而上的DP是解决同一问题的两种不同方法。考虑使用记忆(自上而下)与动态(自下而上)编程解决方案来计算斐波那契数。

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

我个人认为记忆更加自然。您可以采用递归函数并通过机械过程来记忆它(首先在缓存中查找答案并在可能的情况下返回它,否则递归计算它,然后在返回之前将计算保存在缓存中以供将来使用),而进行自下而上动态编程要求您对计算解决方案的顺序进行编码,这样就不会在它所依赖的较小问题之前计算“大问题”。

Top down and bottom up DP are two different ways of solving the same problems. Consider a memoized (top down) vs dynamic (bottom up) programming solution to computing fibonacci numbers.

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

I personally find memoization much more natural. You can take a recursive function and memoize it by a mechanical process (first lookup answer in cache and return it if possible, otherwise compute it recursively and then before returning, you save the calculation in the cache for future use), whereas doing bottom up dynamic programming requires you to encode an order in which solutions are calculated, such that no "big problem" is computed before the smaller problem that it depends on.

难忘№最初的完美 2024-11-17 07:01:15

动态规划的一个关键特征是存在重叠子问题。也就是说,您试图解决的问题可以分解为子问题,并且许多子问题共享子子问题。这就像“分而治之”,但你最终会多次做同样的事情。我自 2003 年以来在教学或解释这些问题时使用的一个例子:你可以递归地计算 斐波那契数

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

使用您最喜欢的语言并尝试为 fib(50) 运行它。这将需要非常非常长的时间。大约与 fib(50) 本身一样多的时间!然而,许多不必要的工作正在进行中。 fib(50) 将调用 fib(49)fib(48),但是这两个最终都会调用 fib (47),即使值相同。事实上,fib(47) 将被计算三次:通过 fib(49) 直接调用、通过 fib(48)< 直接调用/code>,也可以通过另一个 fib(48) 的直接调用,该调用是由 fib(49) 的计算生成的...所以你看,我们有重叠的子问题

好消息:无需多次计算相同的值。计算一次后,缓存结果,下次使用缓存的值!这就是动态规划的本质。您可以将其称为“自上而下”、“记忆”或任何您想要的名称。这种方法非常直观并且非常容易实现。只需首先编写一个递归解决方案,在小测试中对其进行测试,添加记忆(缓存已计算值),然后——宾果! ---你已经完成了。

通常,您还可以编写一个等效的迭代程序,该程序自下而上运行,无需递归。在这种情况下,这将是更自然的方法:从 1 到 50 循环计算所有斐波那契数。

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

在任何有趣的场景中,自下而上的解决方案通常更难以理解。然而,一旦你真正理解了它,通常你会对算法的工作原理有一个更清晰的了解。在实践中,在解决重要问题时,我建议首先编写自上而下的方法并在小示例上进行测试。然后编写自下而上的解决方案并比较两者以确保您得到相同的结果。理想情况下,自动比较两个解决方案。编写一个小例程,理想情况下会生成大量测试 - 所有 达到一定大小的小测试 - 并验证两种解决方案是否给出相同的结果。之后在生产中使用自下而上的解决方案,但保留自上而下的代码,注释掉。这将使其他开发人员更容易理解您正在做什么:自下而上的代码可能非常难以理解,即使您编写了它,即使您确切地知道自己在做什么。

在许多应用程序中,由于递归调用的开销,自下而上的方法稍微快一些。堆栈溢出也可能是某些问题中的一个问题,请注意,这在很大程度上取决于输入数据。在某些情况下,如果您不太了解动态编程,您可能无法编写导致堆栈溢出的测试,但有一天这种情况仍然可能发生。

现在,存在一些问题,自上而下的方法是唯一可行的解​​决方案,因为问题空间太大,不可能解决所有子问题。然而,“缓存”仍然在合理的时间内起作用,因为您的输入只需要解决一小部分子问题——但是显式定义您需要解决哪些子问题并因此编写一个底层太棘手了解决方案。另一方面,在某些情况下,您知道需要解决所有子问题。在这种情况下,继续使用自下而上的方法。

我个人会使用从上到下的段落优化,即 自动换行优化问题(查找 Knuth -Plas 断行算法;至少 TeX 使用它,Adobe Systems 的一些软件也使用类似的方法)。我会使用自下而上的快速傅里叶变换

A key feature of dynamic programming is the presence of overlapping subproblems. That is, the problem that you are trying to solve can be broken into subproblems, and many of those subproblems share subsubproblems. It is like "Divide and conquer", but you end up doing the same thing many, many times. An example that I have used since 2003 when teaching or explaining these matters: you can compute Fibonacci numbers recursively.

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

Use your favorite language and try running it for fib(50). It will take a very, very long time. Roughly as much time as fib(50) itself! However, a lot of unnecessary work is being done. fib(50) will call fib(49) and fib(48), but then both of those will end up calling fib(47), even though the value is the same. In fact, fib(47) will be computed three times: by a direct call from fib(49), by a direct call from fib(48), and also by a direct call from another fib(48), the one that was spawned by the computation of fib(49)... So you see, we have overlapping subproblems.

Great news: there is no need to compute the same value many times. Once you compute it once, cache the result, and the next time use the cached value! This is the essence of dynamic programming. You can call it "top-down", "memoization", or whatever else you want. This approach is very intuitive and very easy to implement. Just write a recursive solution first, test it on small tests, add memoization (caching of already computed values), and --- bingo! --- you are done.

Usually you can also write an equivalent iterative program that works from the bottom up, without recursion. In this case this would be the more natural approach: loop from 1 to 50 computing all the Fibonacci numbers as you go.

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

In any interesting scenario the bottom-up solution is usually more difficult to understand. However, once you do understand it, usually you'd get a much clearer big picture of how the algorithm works. In practice, when solving nontrivial problems, I recommend first writing the top-down approach and testing it on small examples. Then write the bottom-up solution and compare the two to make sure you are getting the same thing. Ideally, compare the two solutions automatically. Write a small routine that would generate lots of tests, ideally -- all small tests up to certain size --- and validate that both solutions give the same result. After that use the bottom-up solution in production, but keep the top-bottom code, commented out. This will make it easier for other developers to understand what it is that you are doing: bottom-up code can be quite incomprehensible, even you wrote it and even if you know exactly what you are doing.

In many applications the bottom-up approach is slightly faster because of the overhead of recursive calls. Stack overflow can also be an issue in certain problems, and note that this can very much depend on the input data. In some cases you may not be able to write a test causing a stack overflow if you don't understand dynamic programming well enough, but some day this may still happen.

Now, there are problems where the top-down approach is the only feasible solution because the problem space is so big that it is not possible to solve all subproblems. However, the "caching" still works in reasonable time because your input only needs a fraction of the subproblems to be solved --- but it is too tricky to explicitly define, which subproblems you need to solve, and hence to write a bottom-up solution. On the other hand, there are situations when you know you will need to solve all subproblems. In this case go on and use bottom-up.

I would personally use top-bottom for Paragraph optimization a.k.a the Word wrap optimization problem (look up the Knuth-Plass line-breaking algorithms; at least TeX uses it, and some software by Adobe Systems uses a similar approach). I would use bottom-up for the Fast Fourier Transform.

小耗子 2024-11-17 07:01:15

让我们以斐波那契数列为例

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

另一种说法,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

在前五个斐波那契数的情况下

Bottom(first) number :1
Top (fifth) number: 5 

现在让我们以递归斐波那契数列算法为例

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

使用以下命令执行该程序

rcursive(5);

现在,如果我们仔细研究该算法,则 :为了生成第五个数字,需要第三个和第四个数字。所以我的递归实际上从顶部(5)开始,然后一直到底部/较低的数字。这种方法实际上是自上而下的方法。

为了避免多次进行相同的计算,我们使用动态编程技术。我们存储先前计算的值并重用它。这种技术称为记忆化。除了记忆之外,动态编程还有更多内容,讨论当前问题不需要记忆。

自上而下

让我们重写我们的原始算法并添加记忆技术。

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

我们像下面一样执行这个方法。

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

这个解决方案仍然是自上而下的,因为算法从顶部值开始,每一步都走到底部以获得我们的顶部值。

自下而上

但是,问题是,我们能否从底部开始,例如从第一个斐波那契数开始,然后向上走。让我们使用这种技术重写它,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

现在如果我们研究这个算法,它实际上是从较低的值开始,然后转到顶部。如果我需要第五个斐波那契数,我实际上正在计算第一个,然后是第二个,然后是第三个,一直到第五个数。这种技术实际上称为自下而上的技术。

最后两个,算法完全满足动态规划的要求。但一种是自上而下的,另一种是自下而上的。两种算法具有相似的空间和时间复杂度。

Lets take fibonacci series as an example

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

Another way to put it,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

In case of first five fibonacci number

Bottom(first) number :1
Top (fifth) number: 5 

Now lets take a look of recursive Fibonacci series algorithm as an example

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

Now if we execute this program with following commands

rcursive(5);

if we closely look into the algorithm, in-order to generate fifth number it requires 3rd and 4th numbers. So my recursion actually start from top(5) and then goes all the way to bottom/lower numbers. This approach is actually top-down approach.

To avoid doing same calculation multiple times we use Dynamic Programming techniques. We store previously computed value and reuse it. This technique is called memoization. There are more to Dynamic programming other then memoization which is not needed to discuss current problem.

Top-Down

Lets rewrite our original algorithm and add memoized techniques.

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

And we execute this method like following

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

This solution is still top-down as algorithm start from top value and go to bottom each step to get our top value.

Bottom-Up

But, question is, can we start from bottom, like from first fibonacci number then walk our way to up. Lets rewrite it using this techniques,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

Now if we look into this algorithm it actually start from lower values then go to top. If i need 5th fibonacci number i am actually calculating 1st, then second then third all the way to up 5th number. This techniques actually called bottom-up techniques.

Last two, algorithms full-fill dynamic programming requirements. But one is top-down and another one is bottom-up. Both algorithm has similar space and time complexity.

作业与我同在 2024-11-17 07:01:15

动态规划问题可以使用自下而上或自上而下的方法来解决。

一般来说,自下而上的方法使用制表技术,而自上而下的方法使用递归(带记忆)技术。

但您也可以使用递归来采用自下而上和自上而下的方法,如下所示。

自下而上:从基本条件开始,递归地传递到目前为止计算出的值。一般来说,这些都是尾递归。

int n = 5;
fibBottomUp(1, 1, 2, n);

private int fibBottomUp(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibBottomUp(j, i + j, count + 1, n);
}

自上而下:从最终条件开始,递归地得到其子问题的结果。

int n = 5;
fibTopDown(n);

private int fibTopDown(int n) {
    if (n <= 1) return 1;
    return fibTopDown(n - 1) + fibTopDown(n - 2);
}

Dynamic programming problems can be solved using either bottom-up or top-down approaches.

Generally, the bottom-up approach uses the tabulation technique, while the top-down approach uses the recursion (with memorization) technique.

But you can also have bottom-up and top-down approaches using recursion as shown below.

Bottom-Up: Start with the base condition and pass the value calculated until now recursively. Generally, these are tail recursions.

int n = 5;
fibBottomUp(1, 1, 2, n);

private int fibBottomUp(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibBottomUp(j, i + j, count + 1, n);
}

Top-Down: Start with the final condition and recursively get the result of its sub-problems.

int n = 5;
fibTopDown(n);

private int fibTopDown(int n) {
    if (n <= 1) return 1;
    return fibTopDown(n - 1) + fibTopDown(n - 2);
}
ヅ她的身影、若隐若现 2024-11-17 07:01:15

动态规划通常称为记忆化!

1.记忆化是自上而下的技术(通过分解来开始解决给定的问题),动态规划是自下而上的技术(从琐碎的子问题开始解决,向上解决给定的问题)

2.DP找到解决方案从基本案例开始并向上进行。 DP 解决了所有子问题,因为它是自下而上的

与 Memoization 不同,Memoization 仅解决所需的子问题

  1. ,DP 具有将指数时间暴力解决方案转换为多项式时间算法的潜力。

  2. DP 可能更高效,因为它的迭代

相反,Memoization 必须支付由于递归而产生的(通常是很大的)开销。

更简单地说,Memoization 使用自上而下的方法来解决问题,即从核心(主要)问题开始,然后将其分解为子问题,并以类似的方式解决这些子问题。在这种方法中,相同的子问题可能会出现多次并消耗更多的CPU周期,从而增加时间复杂度。而在动态规划中,相同的子问题不会被多次解决,但先前的结果将用于优化解决方案。

Dynamic Programming is often called Memoization!

1.Memoization is the top-down technique(start solving the given problem by breaking it down) and dynamic programming is a bottom-up technique(start solving from the trivial sub-problem, up towards the given problem)

2.DP finds the solution by starting from the base case(s) and works its way upwards. DP solves all the sub-problems, because it does it bottom-up

Unlike Memoization, which solves only the needed sub-problems

  1. DP has the potential to transform exponential-time brute-force solutions into polynomial-time algorithms.

  2. DP may be much more efficient because its iterative

On the contrary, Memoization must pay for the (often significant) overhead due to recursion.

To be more simple, Memoization uses the top-down approach to solve the problem i.e. it begin with core(main) problem then breaks it into sub-problems and solve these sub-problems similarly. In this approach same sub-problem can occur multiple times and consume more CPU cycle, hence increase the time complexity. Whereas in Dynamic programming same sub-problem will not be solved multiple times but the prior result will be used to optimize the solution.

╭ゆ眷念 2024-11-17 07:01:15

简单地说,自上而下的方法使用递归来一次又一次地调用子问题
,而自下而上的方法使用单个而不调用任何一个,因此它更有效。

Simply saying top down approach uses recursion for calling Sub problems again and again
where as bottom up approach use the single without calling any one and hence it is more efficient.

如何视而不见 2024-11-17 07:01:15

以下是基于 DP 的自上而下的编辑距离问题解决方案。我希望它也有助于理解动态编程的世界:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

你可以在家里想象它的递归实现。如果您以前没有解决过类似的问题,那么这是非常好的且具有挑战性的。

Following is the DP based solution for Edit Distance problem which is top down. I hope it will also help in understanding the world of Dynamic Programming:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

You can think of its recursive implementation at your home. It's quite good and challenging if you haven't solved something like this before.

还不是爱你 2024-11-17 07:01:15

免责声明:我仍在学习这个主题,我的答案可能不正确......

我觉得这里的答案具有误导性或没有澄清混淆点。

自顶向下

可以轻松地以树形形式可视化

start:

Disclaimer: I’m still learning this subject, my answer maybe incorrect…

I feel like answers here are misleading or don't clarify the confusion points.

Top-down

Can easily be visualized in a tree form

start:???? 
                                ???? F(5)
                                 /     \
                              F(4)     F(3)
                            /     \   /     \
                         F(3)   F(2) F(2)   F(1)
                        /   \   / \   / \
                     F(2) F(1)F(1)F(0)F(1)F(0)
                    /   \
                 F(1)  F(0)

You start from the value need. Its value is known to be dependent on another value. You keep going down until you hit a base case. Then you backtrack/unwind/bubble up and return a value. One might argue that you should name this top down and then back up again

With some over simplification, you could technically perceive this in a table like form as well:

start:????
in call stack: 〰️

(0,1 are base cases. their values are known)
                                ????
Index:  0    1    2    3    4    5
Value: [0]  [1]  [ ]  [ ]  [ ]  [ ]

After Step 2:         〰️   〰️   ????
Value: [0]  [1]  [ ]  [ ]  [ ]  [ ]

After Step 3:
            〰️   〰️   〰️   〰️  ????
Value: [0]  [1]  [1]  [ ]  [ ]  [ ]

After Step 4:
       〰️   〰️   〰️   〰️   〰️  ????
Value: [0]  [1]  [1]  [ ]   [ ] [ ]

After Step 5: (back track ????)
Value: [0]  [1]  [1]  [2]  [3]  [5]

Top in this context means, the value that's build on top (off) of all other values. Don't think of it as left or right.

The start value hasn't really moved. Because its value isn't retrieved until its dependencies are computed.

Bottom-up

The traversal can't truely be visualized in a tree form. Since in trees you start from the root, not a leaf. It's better to think of it a iterative build up of your subproblems.

start:????

       ????
Index:  0    1    2    3    4    5
Value: [0]  [1]  [ ]  [ ]  [ ]  [ ]

After Step 2:
Value: [0]  [1]  [1]  [ ]  [ ]  [ ]

After Step 3:
Value: [0]  [1]  [1]  [2]  [ ]  [ ]

After Step 4:
Value: [0]  [1]  [1]  [2]  [3]  [ ]

After Step 5:
Value: [0]  [1]  [1]  [2]  [3]  [5]

Once you finish and get to the correct level. You stop iterating. You just return the value.

Bottom in this context means, the value that's everything else is built on top of. Don't think of it as left or right.

Could the naming be more clear?

Everyone's different, personally prefer to think of them as:

  • recursive (top down). Seek answer to "big problem" first.
  • tabulation (bottom up); See answer to "smaller problems (closer to base cases)" first.

With both approaches you ALWAYS end up calculating smaller values first. There's no way around that. The number of calculations are also the same. Only that the manner they're invoked is different.

Note

You could possibly visualize a bottom-up approach as such though. However with trees your traversal begins from the root. So technically you can't construct (nor traverse) an empty tree from a leaf.

????: the value you're calculating

Step 1: Compute F(2)

               ????  F(2)
                  /    \
               F(1)   F(0)

Step 2: Compute F(3)


               ????  F(3)
                  /    \
               F(2)   F(1)
             /     \
          F(1)    F(0)

Step 3: Compute F(4)


             ????    F(4)
                  /    \
              F(3)   F(2)
            /     \   /    \
         F(2)   F(1)F(1)  F(0)
        /    \
    F(1)   F(0)

Step 4: Compute F(5)


                ???? F(5)
                  /    \
                F(4)   F(3)
            /     \   /    \
         F(3)   F(2)F(2)  F(1)
        /    \   /  \   /   \
     F(2)  F(1)F(1)F(0)F(1)F(0)
    /    \
F(1)   F(0)

Quiz

Say you were asked to calculate the number of 2s in an array of n items:

Find out which approach is top down vs bottom up:

SolutionA:
Start count from the beginning. Increment count as you find one. Return the final count at the end.

answer = 0

// start at index 0
for i in 0..<n
if array[i] == 2 {
   answer += 1
}

return answer

SolutionB:
Start counting from the end, Increment the count as you find one. Return the final count at the end.

answer = 0

// start at index n - 1
for i in (0..<n).reversed
if array[i] == 2 {
   answer += 1
}

return answer

SolutionC:
Return the total count at the last index based off its previous answers



// helper
func getCount(at index: Int) -> Int {
    if index == 0 {
       return array[index] == 2 ? 1 : 0
    }
    if array[index] == 2  {
       return getCount(at: index - 1) + 1
    } else {
       return getCount(at: index - 1)
    }
}

// return the count at index (n - 1)
return getCount(at: array.count - 1) 

Answers:

A: Bottom up: It's an iterative approach where we build up all results starting from the first element. Then return the answer for the desired index.
B: Also Bottom up: Just because we changed the direction doesn't at started from the other direction it doesn't make it non-iterative. It's still the same. Direction isn't the key element here. The key element is the iterative accumulation of the result and building up our answer step by step
C: Top down: Because we inquire the exact and final value needed. It ends up recursively calling its smaller sub problems. Each return its value, until you hit a base case.

ま昔日黯然 2024-11-17 07:01:15

没什么可困惑的...您通常以自下而上的方式学习语言(从基础知识到更复杂的事情),并且经常以自上而下的方式制作您的项目(从总体目标和代码结构到某些部分)实现)

如果涉及逻辑代码示例 - 此处

nothing to be confused about... you usually learn the language in bottom-up manner (from basics to more complicated things), and often make your project in top-down manner (from overall goal & structure of the code to certain pieces of implementations)

If concerning logical code examples - here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文