分治算法与动态规划的区别

Posted

技术标签:

【中文标题】分治算法与动态规划的区别【英文标题】:Difference between Divide and Conquer Algo and Dynamic Programming 【发布时间】:2012-11-12 08:55:49 【问题描述】:

分治算法和动态规划算法有什么区别?这两个术语有何不同?我不明白它们之间的区别。

请举一个简单的例子来解释两者之间的任何区别以及它们看起来相似的原因。

【问题讨论】:

【参考方案1】:

分而治之

分而治之的工作方式是将问题划分为子问题,递归地征服每个子问题并将这些解决方案组合起来。

动态编程

动态编程是一种解决重叠子问题的技术。每个子问题只解决一次,每个子问题的结果存储在一个表中(通常实现为数组或哈希表)以供将来参考。这些子解决方案可以用来获得原始解决方案,存储子问题解决方案的技术称为记忆。

你可能会想到DP = recursion + re-use

了解差异的一个经典示例是查看这两种方法来获得第 n 个斐波那契数。检查来自麻省理工学院的material。


分而治之的方法

动态编程方法

【讨论】:

你是如何制作这些图像的?用鼠标? 我认为整个答案中最重要的一行是:“重叠子问题”。 DP有,分而治之没有 @HasanIqbalAnik 重叠子问题是指反复出现的问题。就像在上面显示的示例中解决 fn-2 一样。所以在 D&C 中它就在那里,这就是为什么它不如 DP 高效。 奇怪! “重叠子问题”你在谈论这个问题,但“动态编程”是一种算法。我认为区分“问题”和“算法”很重要。 是的,DP 记忆重叠部分以获得优于分而治之的优势。【参考方案2】:

动态规划和分而治之的相似性

就我现在所见,我可以说动态编程是分而治之范式的扩展

我不会将它们视为完全不同的东西。因为它们都通过递归地将问题分解为相同或相关类型的两个或多个子问题来工作,直到这些子问题变得简单到可以直接解决。然后将子问题的解决方案组合起来,得到原始问题的解决方案。

那么为什么我们仍然有不同的范例名称以及为什么我将动态编程称为扩展。这是因为动态规划方法可以应用于问题只有当问题具有某些限制或先决条件。之后,动态编程通过 memoizationtabulation 技术扩展了分而治之的方法。

让我们一步一步来……

动态编程先决条件/限制

正如我们刚刚发现的,为了使动态规划适用,分而治之问题必须具备两个关键属性:

最优子结构 — 最优解可以从其子问题的最优解构造

重叠的子问题 — 问题可以分解为多次重复使用的子问题,或者问题的递归算法一遍又一遍地解决相同的子问题,而不是总是产生新的子问题

一旦满足这两个条件,我们可以说这个分而治之的问题可以使用动态规划方法来解决。

分而治之的动态编程扩展

动态编程方法通过两种技术(memoizationtabulation)扩展了分而治之的方法,这两种技术的目的都是为了存储和重用可能存在的子问题解决方案大幅度提高性能。例如,斐波那契函数的朴素递归实现的时间复杂度为 O(2^n),其中 DP 解决方案仅使用 O(n) 时间。

记忆化(自上而下的缓存填充)是指缓存和重用先前计算结果的技术。记忆化的fib 函数因此看起来像这样:

memFib(n) 
    if (mem[n] is undefined)
        if (n < 2) result = n
        else result = memFib(n-2) + memFib(n-1)
        
        mem[n] = result
    return mem[n]

制表(自下而上的缓存填充) 类似,但侧重于填充缓存的条目。计算缓存中的值最容易迭代完成。 fib 的制表版本如下所示:

tabFib(n) 
    mem[0] = 0
    mem[1] = 1
    for i = 2...n
        mem[i] = mem[i-2] + mem[i-1]
    return mem[n]

您可以阅读有关记忆和制表比较的更多信息here。

您应该在这里掌握的主要思想是,由于我们的分而治之问题具有重叠的子问题,子问题解决方案的缓存成为可能,因此记忆/制表步入了现场。

那么DP和DC到底有什么区别

由于我们现在熟悉 DP 先决条件及其方法,我们准备将上述所有内容整合到一张图片中。

如果您想查看代码示例,您可以查看more detailed explanation here,其中有两个算法示例:二分搜索和最小编辑距离(Levenshtein 距离),它们说明了 DP 和 DC 之间的区别。

【讨论】:

题外话:你是用绘图板画的吗? @GeonGeorge 不,这幅画是用钢笔画的,然后扫描的 这是我读过的关于组织 DP 的最佳答案之一 这就是动态编程的教学方式!【参考方案3】:

分而治之和动态规划之间的另一个区别可能是:

分而治之:

    在子问题上做更多的工作,因此需要更多的时间。 在分而治之中,子问题相互独立。

动态编程:

    只解决子问题一次,然后将其存储在表中。 在动态规划中,子问题不是独立的。

【讨论】:

分而治之的算法不一定比它们的 DP 替代方案做更多的工作。一个例子是 Erickson 的求最大算术级数算法。【参考方案4】:

有时在递归编程时,您会多次调用具有相同参数的函数,这是不必要的。

著名的斐波那契数例:

           index: 1,2,3,4,5,6...
Fibonacci number: 1,1,2,3,5,8...

function F(n) 
    if (n < 3)
        return 1
    else
        return F(n-1) + F(n-2)

让我们运行 F(5):

F(5) = F(4) + F(3)
     = F(3)+F(2) + F(2)+F(1)
     = [F(2)+F(1)]+1 + 1+1
     = 1+1+1+1+1

所以我们调用了: 1 倍 F(4) 2 次 F(3) 3 次 F(2) 2次F(1)

动态编程方法:如果多次调用具有相同参数的函数,则将结果保存到变量中,以便下次直接访问。迭代方式:

if (n==1 || n==2)
    return 1
else
    f1=1, f2=1
    for i=3 to n
         f = f1 + f2
         f1 = f2
         f2 = f

让我们再次调用 F(5):

fibo1 = 1
fibo2 = 1 
fibo3 = (fibo1 + fibo2) = 1 + 1 = 2
fibo4 = (fibo2 + fibo3) = 1 + 2 = 3
fibo5 = (fibo3 + fibo4) = 2 + 3 = 5

如您所见,每当您需要多次调用时,您只需访问相应的变量即可获取值,而不是重新计算它。

顺便说一句,动态编程并不意味着将递归代码转换为迭代代码。如果需要递归代码,还可以将子结果保存到变量中。在这种情况下,该技术称为记忆化。对于我们的示例,它看起来像这样:

// declare and initialize a dictionary
var dict = new Dictionary<int,int>();
for i=1 to n
    dict[i] = -1

function F(n) 
    if (n < 3)
        return 1
    else
    
        if (dict[n] == -1)
            dict[n] = F(n-1) + F(n-2)

        return dict[n]                
    

所以与分治法的关系是 D&D 算法依赖于递归。它们的某些版本具有这种“具有相同参数问题的多个函数调用”。搜索“矩阵链乘法”和“最长公共子序列”以获取需要 DP 来改进 D&D 算法的 T(n) 的示例。

【讨论】:

【参考方案5】:

我假设您已经阅读过 Wikipedia 和其他学术资源,因此我不会回收任何这些信息。我还必须声明,无论如何我都不是计算机科学专家,但我会分享我对这些主题的理解的两分钱......

动态规划

将问题分解为离散的子问题。斐波那契数列的递归算法是动态规划的一个例子,因为它通过首先求解 fib(n-1) 来求解 fib(n)。为了解决原来的问题,它解决了一个不同的问题。

分而治之

这些算法通常会解决问题的相似部分,然后在最后将它们组合在一起。合并排序是分而治之的典型例子。这个例子和斐波那契例子的主要区别在于,在归并排序中,分割(理论上)可以是任意的,无论你如何分割它,你仍然在归并排序。必须完成相同数量的工作 来对数组进行归并排序,无论您如何划分它。求解 fib(52) 比求解 fib(2) 需要更多步骤

【讨论】:

【参考方案6】:

我认为Divide &amp; Conquer 是一种递归方法,Dynamic Programming 是表格填充。

例如,Merge Sort 是一个Divide &amp; Conquer 算法,因为在每一步中,您将数组分成两半,在两半上递归调用Merge Sort,然后合并它们。

Knapsack 是一个Dynamic Programming 算法,因为您正在填写一个代表整个背包子问题的最佳解决方案的表格。表中的每个条目对应于在给定项目 1-j 的情况下,您可以在一袋重量 w 中携带的最大值。

【讨论】:

虽然这在很多情况下都是正确的,但我们将子问题的结果存储在一个表中并不总是正确的。【参考方案7】:

分而治之在每个递归级别都涉及三个步骤:

    将问题划分为子问题。 通过递归解决子问题征服子问题。 子问题的解决方案合并到原始问题的解决方案中。 这是一种自上而下的方法。 它在子问题上做更多的工作,因此有更多的时间 消费。 例如。斐波那契数列的第 n 项可以以 O(2^n) 时间复杂度计算。

动态规划包括以下四个步骤: 1. 表征最优解的结构。 2. 递归定义最优解的值。 3. 计算最优解的值。 4. 构造根据计算信息优化解决方案。

这是一种自下而上的方法。 比分治法消耗更少的时间,因为我们使用了之前计算的值,而不是再次计算。 例如。斐波那契数列的第 n 项可以以 O(n) 时间复杂度计算。

为了更容易理解,让我们将分而治之视为一种蛮力解决方案,并将其优化视为动态规划。 注意具有重叠子问题的分治算法只能使用 dp 进行优化。

【讨论】:

分而治之是自下而上,动态规划是自上而下【参考方案8】: 分而治之 他们闯入了不重叠的子问题 示例:阶乘数,即 fact(n) = n*fact(n-1)
fact(5) = 5* fact(4) = 5 * (4 * fact(3))= 5 * 4 * (3 *fact(2))= 5 * 4 * 3 * 2 * (fact(1))

正如我们在上面看到的,没有事实(x)重复,因此阶乘没有重叠问题。

动态编程 他们分成重叠的子问题 示例:斐波那契数列,即 fib(n) = fib(n-1) + fib(n-2)
fib(5) = fib(4) + fib(3) = (fib(3)+fib(2)) + (fib(2)+fib(1))

正如我们在上面看到的,fib(4) 和 fib(3) 都使用 fib(2)。同样,如此多的 fib(x) 被重复。这就是斐波那契有重叠子问题的原因。

由于DP中子问题的重复,我们可以将这样的结果保存在一个表中并节省计算工作量。这被称为 memoization

【讨论】:

【参考方案9】:

分而治之

在此问题中通过以下三个步骤解决: 1。 Divide - 划分子问题的数量 2。征服 - 通过递归解决子问题来征服 3。合并 - 合并子问题的解决方案以获得原始问题的解决方案 递归方法 自上而下的技巧 示例:合并排序

动态编程

在此问题通过以下步骤解决: 1. 定义最优解的结构 2. 反复定义最优解的值。 3. 自下而上获取最优解的值 4. 从得到的值中得到最终的最优解 非递归 自下而上的技巧 示例: Strassen 的矩阵乘法

【讨论】:

您的答案是下面@Neel Alex 的答案。 !!!! 我在回答之前没有检查,可能是我当时错过了。但是同样的问题可以有同样的答案,因为网上有不同的免费学习资源。

以上是关于分治算法与动态规划的区别的主要内容,如果未能解决你的问题,请参考以下文章

动态规划和分治区别?

动态规划和分治区别?

分治法动态规划贪心算法区别

python_分治算法贪心算法动态规划算法

动态规划算法学习总结

算法1:动态规划