递归、记忆化和动态规划有什么区别?

72
相关问题:
动态规划和记忆化:自顶向下 vs 自底向上
我已经阅读了很多关于这个的文章,但似乎无法理解。有时递归和动态规划看起来相同,而在其他情况下,记忆化和动态规划看起来类似。有人能解释一下它们之间的区别吗?
P.S. 如果您可以指向使用三种方法解决相同问题的代码,那将非常有帮助。(例如,斐波那契数列问题,我认为我阅读的每篇文章都使用递归,但称其为动态规划)

6
有何不同之处呢? :) - cheeken
3
关于递归,请查看这个问题。 - Anne
首先尝试理解递归是什么。过一段时间后,你也会理解动态规划。 - Rsh
3
重复问题是什么?有人能提供链接吗?这应该带有“标记为重复”的标志。 - OKGimmeMoney
5个回答

82

考虑计算斐波那契数列:

纯递归:

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

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

会导致指数级别的调用次数。

使用记忆化/动态规划的递归:

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;
}

现在我们有线性数量的调用,第一次是线性的,以后都是常数时间。

上述方法被称为“延迟计算”。我们在第一次需要时计算早期项。

以下方法也被视为动态规划,但不使用递归:

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;
}

这种方法使用恒定的空间和线性时间。

此外,为了完整起见,我想提到一个闭合形式的斐波那契数列计算方法,它既不使用递归也不使用动态规划,而是使用基于黄金比例的数学公式,使我们能够在常数时间内计算出斐波那契数列的项:

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


11
你最后一个例子仍然是DP,只是减少了存储空间。算法与你前两个例子中的相同。 - IVlad
3
大多数人不会认为最后一个编码答案是DP,他们会称之为简单的迭代解决方案。值得注意的是,它不会跟踪术语编号和该术语答案之间的映射关系。最终没有明确的测试能够确定某个算法是否为DP。通常情况下,不缓存答案的纯递归(这就是记忆化所表示的全部)被认为不是DP。 - Andrew Tomazos
@AndrewTomazos,您能否解释一下为什么第二个例子是DP?我知道这是递归,但不明白为什么它是DP。 - user13107
1
它将答案存储在缓存中,因此如果进行两次调用 f(3),然后稍后再次调用 f(3),只有第一次进行计算,第二次调用从第一次缓存的结果获取。这通常被认为是DP的一种形式。 - Andrew Tomazos
这相当于Hirschberg算法在线性空间中计算Levenshtein距离,尽管完整的DP矩阵具有O(nm)个条目。 - Sebastian Graf
显示剩余8条评论

45

递归加上记忆化正是动态规划的一种具体“口味”:按照自顶向下的方式进行动态规划。

更确切地说,并没有要求特别使用递归。任何与记忆化结合的分治算法都是自顶向下的动态规划。(递归是分治法中后进先出(LIFO)的一种,“先进先出”(FIFO)或其他任何类型的分治法也可以使用。)

因此,更准确地说:

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

另外,从非常正式的角度来看,如果你为一个不生成重复子问题(意味着没有记忆化优势)的问题实现了分治算法,那么你可以声称这个分治算法是“动态规划”的一种退化示例。

然而,动态规划是一个更一般的概念。动态规划可以使用自底向上的方法,这与分治+记忆化不同。


3
自底向上的方法计算整个矩阵,无论实际上是否需要结果,而自顶向下的方法更像是惰性求值:只有在需要时才计算结果,但大多数情况下,与此类缓存相关的簿记工作被基于数组的代码的访问模式和适当并行化的能力所超越。 - Sebastian Graf

23

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

递归是再次调用自身。

动态规划是一种解决具有特定结构(最优子结构)问题的方法,其中问题可以被分解为与原始问题类似的子问题。显然,可以使用递归来解决DP问题。但是这并不是必需的。可以在没有递归的情况下解决DP问题。

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


所以,我的理解是递归和记忆化用于解决动态规划问题,但它们是完全不同的东西。而分治问题与动态规划的区别在于子问题不重叠。 - Sakibul Alam
@Shuvo 是的。DP是一种分治算法。你说得对,在DP中我们最终会有重叠的子问题。我们利用这个事实,通过将子解决方案存储在表格中来节省时间。 - Ankush

10
他们是不同的概念。它们经常重叠,但它们是不同的。
递归发生在函数直接或间接地调用自身时。就是这样。
例如:
a -> call a
a -> call b, b -> call c, c -> call a

动态规划是指利用较小子问题的解来解决更大问题的方法。通常情况下,这种解法最容易通过递归实现,因为你通常会以递归函数的形式考虑这种解法。但是,迭代实现通常更受欢迎,因为它需要更少的时间和内存。

记忆化技术用于避免递归DP实现所需的时间过长。大多数情况下,DP算法将在解决多个大问题时使用相同的子问题。在递归实现中,这意味着我们将多次重新计算相同的内容。记忆化意味着将这些子问题的结果保存到表中。在进入递归调用时,我们检查其结果是否存在于表中:如果是,则返回该结果;如果不是,则计算它,将其保存在表中,然后返回它。


-6

6
DP(几乎?)总是涉及到递归和记忆化,因此说它们之间没有任何联系是不正确的。 - BlueRaja - Danny Pflughoeft
@BlueRaja-DannyPflughoeft: 你误解了我的意思:这就是为什么我说“...它们是完全独立的概念。” 作为澄清。你总是可以将递归算法转换为迭代算法。DP和记忆化利用最优子结构的优势;这并不意味着它们本质上是递归的;递归只是一种视角来看待最优子结构的利用。鸽子倾向于栖息在建筑物上这个事实并不意味着鸽子是与建筑物相关的概念,除非你正在争论这一点,那么这没问题。 - ninjagecko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接