【问题标题】:What's the difference between recursion, memoization & dynamic programming? [duplicate]递归、记忆和动态编程有什么区别? [复制]
【发布时间】:2012-08-21 10:55:53
【问题描述】:

可能重复:
Dynamic programming and memoization: top-down vs bottom-up approaches

我已经阅读了很多关于此的文章,但似乎无法理解。有时递归和动态编程看起来一样,而在其他时候,记忆化和动态编程看起来很相似。谁能给我解释一下有什么区别?

附:如果您可以在同一问题上使用这三种方法向我指出一些代码,那也会很有帮助。 (例如斐波那契数列问题,我想我读的每一篇文章都使用了递归,但将其称为动态规划)

【问题讨论】:

  • 不是有什么区别? :)
  • 关于递归,请查看this问题。
  • 首先尝试了解什么是递归。过一段时间你也会了解动态编程。
  • 重复的问题是什么?有人可以链接到它吗?这应该带有“标记为重复”标志。

标签: algorithm recursion dynamic-programming memoization


【解决方案1】:

递归与记忆和动态编程绝对无关;这是一个完全独立的概念。

否则,这是一个重复的问题:Dynamic programming and memoization: bottom-up vs top-down approaches

【讨论】:

  • DP(几乎?)总是涉及递归和记忆,所以说它们彼此没有是不正确的。
  • @BlueRaja-DannyPflughoeft:你误解了我所说的:这就是我说“..它们是完全不同的概念”的原因。作为澄清。您始终可以将递归算法转换为迭代算法。 DP和memoization利用optimal substructure;这不会使它们本身递归;递归只是查看最优子结构开发的一种方式。鸽子倾向于栖息在建筑物上这一事实并不能使鸽子成为与建筑物相关的概念,除非您对此提出异议,在这种情况下没关系。
【解决方案2】:

我相信您可以在互联网上找到详细的定义。这是我试图简化的尝试。

递归再次调用自己。

动态规划是一种解决具有特定结构(最佳子结构)的问题的方法,其中问题可以分解为与原始问题相似的子问题。显然,可以调用递归来解决 DP。但这不是必需的。无需递归即可解决 DP。

Memoization 是一种优化依赖递归的 DP 算法的方法。重点不是再次解决已经解决的子问题。您可以将其视为子问题解决方案的缓存。

【讨论】:

  • 所以,我理解的是递归和记忆用于解决 DP 问题,但它们是完全不同的东西。分而治之的问题与 DP 的不同之处在于子问题不重叠。
  • @Shuvo 是的。 DP是分而治之的一种。你说得对,在 DP 中我们最终会遇到重叠的子问题。我们利用这一事实并通过将子解决方案存储在表格中来节省时间。
【解决方案3】:

嗯,递归+记忆正是动态编程的一种特殊“风味”:按照自顶向下方法的动态编程。

更准确地说,不需要专门使用递归。任何与记忆相结合的分治解决方案都是自上而下的动态编程。 (递归是分治法的 LIFO 风格,而您也可以使用 FIFO 分治法或任何其他类型的分治法)。

所以这样说更正确

divide & conquer + memoization == top-down dynamic programming

此外,从非常正式的角度来看,如果您为一个不会产生重复的部分解决方案的问题实施分而治之的解决方案(这意味着记忆化没有好处),那么您可以声称这种分而治之的解决方案解决方案是“动态规划”的退化示例。

然而,动态规划是一个更普遍的概念。动态编程可以采用自底向上的方式,不同于分治+记忆。

【讨论】:

  • 自下而上的方法计算整个矩阵,无论结果是否实际需要,而自上而下的方法更像是惰性求值:只计算结果需要时,但大多数情况下,与这种缓存相关的簿记在访问模式和正确并行化基于数组的代码的能力方面表现出色。
【解决方案4】:

考虑计算斐波那契数列:

纯递归:

int fib(int x)
{
    if (x < 2)
        return 1;

    return fib(x-1) + fib(x-2);
}

导致调用次数呈指数级增长。

使用 memoization/DP 进行递归:

int fib(int x)
{
    static vector<int> cache(N, -1);

    int& result = cache[x];

    if (result == -1)
    {
        if (x < 2)
            result = 1;
        else
            result = fib(x-1) + fib(x-2);
    }

    return result;
}

现在我们第一次有线性调用次数,之后保持不变。

上述方法称为“懒惰”。我们在第一次被要求时计算较早的术语。

以下也将被视为 DP,但没有递归:

int fibresult[N];

void setup_fib()
{
    fibresult[0] = 1;
    fibresult[1] = 1;
    for (int i = 2; i < N; i++)
       fibresult[i] = fibresult[i-1] + fibresult[i-2];
}

int fib(int x) { return fibresult[x]; }

这种方式可以被描述为“渴望”、“预缓存”或“迭代”。它总体上更快,但我们必须手动找出需要计算子问题的顺序。这对于斐波那契来说很容易,但对于更复杂的 DP 问题,它会变得更难,所以如果它很快,我们就会退回到惰性递归方法够了。

以下既不是递归也不是DP:

int fib(int x)
{
    int a = 1;
    int b = 1;
    for (int i = 2; i < x; i++)
    {
        a = a + b;
        swap(a,b);
    }
    return b;
}

它使用恒定空间和线性时间。

为了完整起见,我还要提到斐波那契的封闭形式,它使用下界递归或 DP,它允许我们使用基于黄金比例的数学公式在恒定时间内计算斐波那契项:

http://www.dreamincode.net/forums/topic/115550-fibonacci-closed-form/

【讨论】:

  • 你的最后一个例子 DP,你只是减少了内存。该算法与您前面两个示例中的算法相同。
  • 感谢您提供的示例。在使用记忆之前,纯递归不是 DP 吗?最后一个例子是momoization方法,对吧?
  • 大多数人不会考虑最后的编码答案 DP,他们会称其为简单的迭代解决方案。值得注意的是,它不会跟踪术语编号和该术语的答案之间的映射。最后,没有一个明确的测试可以说是或否某事是 DP 还是不是。不缓存答案的纯递归(这就是记忆的全部含义)通常不被视为 DP。
  • @AndrewTomazos 冷,请解释一下为什么第二个例子是 DP?我知道它是递归,但不明白为什么它的 DP。
  • @user13107:它将答案存储在缓存中,因此如果两次调用f(3),然后再次调用f(3),则只有第一次进行计算,第二次调用从首先。这通常被认为是 DP 的一种形式。
【解决方案5】:

它们是不同的概念。它们经常重叠,但它们是不同的。

只要函数直接或间接调用自身,就会发生递归。就这些了。

例子:

a -> call a
a -> call b, b -> call c, c -> call a

Dynamic programming 是指您使用解决方案来解决较小的子问题以解决较大的问题。这是最容易递归实现的,因为您通常会根据递归函数来考虑此类解决方案。不过,通常首选迭代实现,因为它需要更少的时间和内存。

Memoization 用于防止递归 DP 实现花费比需要更多的时间。大多数时候,DP 算法在解决多个大问题时会使用相同的子问题。在递归实现中,这意味着我们将多次重新计算同一事物。记忆意味着将这些子问题的结果保存到一个表中。当进入一个递归调用时,我们检查它的结果是否存在于表中:如果存在则返回,如果不存在则计算它,保存在表中,然后返回。

【讨论】:

    猜你喜欢
    • 2011-09-05
    • 2010-10-15
    • 2020-12-10
    • 2011-09-22
    • 1970-01-01
    • 2015-08-07
    • 2012-11-12
    相关资源
    最近更新 更多