Divide and Conquer Algorithms和Dynamic Programming Algorithms有什么区别?这两个术语有何不同?我不明白它们之间的区别。
请举一个简单的例子来解释两者之间的差异以及它们看起来相似的基础。
答案 0 :(得分:131)
分而治之
Divide and Conquer通过将问题划分为子问题来工作,递归地征服每个子问题并组合这些解决方案。
动态编程
动态编程是一种解决重叠子问题的技术。每个子问题仅解决一次,并且每个子问题的结果存储在表(通常实现为数组或散列表)中以供将来引用。这些子解可用于获得原始解,而存储子问题解的技术称为memoization。
您可能会想到DP = recursion + re-use
理解差异的一个典型例子是看到这两种获得第n个斐波纳契数的方法。从麻省理工学院查看material。
分而治之的方法
动态规划方法
答案 1 :(得分:21)
分而治之和动态编程之间的另一个区别可能是:
分而治之:
动态编程:
答案 2 :(得分:17)
有时在递归编程时,你会多次调用具有相同参数的函数,这是不必要的。
着名的例子斐波那契数字:
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) F(2)3次 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
正如您所看到的,只要您需要多次调用,您只需访问相应的变量即可获取值而不是重新计算它。
顺便说一下,动态编程并不意味着将递归代码转换为迭代代码。如果需要递归代码,还可以将子结果保存到变量中。在这种情况下,该技术称为memoization。对于我们的示例,它看起来像这样:
// 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&amp; D算法依赖于递归。并且它们的某些版本具有“具有相同参数问题的多个函数调用”。为需要DP来改善D&amp; D算法的T(n)的例子,搜索“矩阵链乘法”和“最长公共子序列”。
答案 3 :(得分:8)
我假设您已经阅读了维基百科及其他学术资源,因此我不会回收任何这些信息。我还必须告诫说,我不是一个计算机科学专家,但我会分享我对这些主题的理解......
将问题分解为离散的子问题。 Fibonacci序列的递归算法是动态规划的一个例子,因为它通过首先求解fib(n-1)来求解fib(n)。为了解决原始问题,它解决了不同的问题。
这些算法通常可以解决问题的类似问题,然后将它们放在一起。 Mergesort是分而治之的典型例子。这个例子与Fibonacci例子的主要区别在于,在mergesort中,除法可以(理论上)是任意的,无论你如何对它进行分割,你仍然在合并和排序。无论你如何分割数组,都必须完成相同数量的工作来合并数组。解决fib(52)需要更多步骤而不是解决fib(2)。
答案 4 :(得分:7)
正如我现在所看到的那样,我可以说动态编程是分而治之的范例的扩展。
我不会将它们视为完全不同的东西。因为它们都通过递归地将问题分解为相同或相关类型的两个或更多个子问题来工作,直到它们变得足够简单直接解决。然后结合子问题的解决方案来解决原始问题。
那么为什么我们仍然有不同的范式名称以及为什么我将动态编程称为扩展。这是因为仅当问题具有某些限制或先决条件时,动态编程方法才可以应用于问题。之后,动态编程通过 memoization 或制表技术扩展了分而治之的方法。
让我们一步一步......
正如我们刚刚发现的那样,为了使动态编程适用,有两个关键属性可以划分和征服问题:
最优子结构 - 最优解可以从其子问题的最优解构造
重叠子问题 - 问题可以分解为多次重复使用的子问题,或者问题的递归算法一遍又一遍地解决相同的子问题,而不是总是生成新的子问题
一旦满足这两个条件,我们可以说这种分而治之的问题可以通过动态编程方法来解决。
动态编程方法通过两种技术( memoization 和制表)扩展了分而治之的方法,这两种技术都有存储和重用子问题解决方案的目的。大幅提升业绩。例如,Fibonacci函数的天真递归实现具有O(2^n)
的时间复杂度,其中DP解决方案仅在O(n)
时间内执行相同的操作。
Memoization(自上而下缓存填充)是指缓存和重用以前计算结果的技术。因此,已记忆的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的先决条件及其方法,我们已准备好将上面提到的所有内容放在一张图片中。
如果您想查看代码示例,可以查看more detailed explanation here,其中您将找到两个算法示例:二进制搜索和最小编辑距离(Levenshtein距离),它们说明了DP和DC之间的差异。
答案 5 :(得分:5)
我认为Divide & Conquer
是递归方法,Dynamic Programming
是表填充。
例如,Merge Sort
是Divide & Conquer
算法,因为在每个步骤中,您将数组拆分为两半,递归调用两半的Merge Sort
然后合并它们。
Knapsack
是一个Dynamic Programming
算法,因为您正在填写表格,表示整个背包的子问题的最佳解决方案。表中的每个条目对应于给定项目1-j的重量包中可以携带的最大值。
答案 6 :(得分:1)
fact(5) = 5* fact(4) = 5 * (4 * fact(3))= 5 * 4 * (3 *fact(2))= 5 * 4 * 3 * 2 * (fact(1))
如上所述,没有事实(x)重复出现,因此阶乘有非重叠问题。
fib(5) = fib(4) + fib(3) = (fib(3)+fib(2)) + (fib(2)+fib(1))
如上所述,fib(4)和fib(3)都使用fib(2)。同样,许多fib(x)被重复。这就是斐波那契有重叠子问题的原因。
答案 7 :(得分:0)
分而治之在每个递归级别都涉及三个步骤:
动态编程涉及以下四个步骤:
1. 特征最佳解决方案的结构。
2. 递归定义最佳解决方案的值。
3. 计算最佳解决方案的价值。
4. 根据计算的信息构造最佳解决方案。
为便于理解,让我们将“分而治之”作为一种蛮力解决方案,并将其优化视为“动态编程”。
NB 具有重叠子问题的分治法只能使用dp进行优化。
答案 8 :(得分:0)
分而治之
动态编程