分而治之算法和动态规划算法有什么区别?这两个术语有何不同?我不明白它们之间的区别。
请举一个简单的例子来解释两者之间的任何区别以及它们看起来相似的原因。
分而治之算法和动态规划算法有什么区别?这两个术语有何不同?我不明白它们之间的区别。
请举一个简单的例子来解释两者之间的任何区别以及它们看起来相似的原因。
分而治之
分而治之的工作方式是将问题划分为子问题,递归地征服每个子问题并将这些解决方案组合起来。
动态规划
动态规划是一种解决具有重叠子问题的问题的技术。每个子问题只解决一次,每个子问题的结果存储在一个表中(通常实现为数组或哈希表)以供将来参考。这些子解决方案可用于获得原始解决方案,并且存储子问题解决方案的技术称为记忆。
你可能会想到DP = recursion + re-use
理解差异的经典示例是查看这两种方法来获得第 n 个斐波那契数。检查麻省理工学院的此材料。
分而治之的方法
动态规划方法
正如我现在所看到的,我可以说动态编程是分而治之范式的扩展。
我不会将它们视为完全不同的东西。因为它们都通过递归地将问题分解为相同或相关类型的两个或多个子问题来工作,直到这些子问题变得足够简单可以直接解决。然后将子问题的解决方案组合起来以给出原始问题的解决方案。
那么为什么我们仍然有不同的范例名称以及为什么我将动态编程称为扩展。这是因为只有当问题具有一定的限制或先决条件时,才可以将动态规划方法应用于问题。之后,动态规划通过记忆或制表技术扩展了分而治之的方法。
让我们一步一步来……</p>
正如我们刚刚发现的那样,为了使动态规划适用,分而治之问题必须具有两个关键属性:
最优子结构 ——最优解可以从其子问题的最优解构造
重叠的子问题 ——问题可以分解成多次重复使用的子问题,或者问题的递归算法一遍又一遍地解决相同的子问题,而不是总是产生新的子问题
一旦满足这两个条件,我们可以说这个分而治之的问题可以使用动态规划方法来解决。
动态编程方法通过两种技术(记忆和制表)扩展了分而治之的方法,这两种技术的目的都是为了存储和重用可以显着提高性能的子问题解决方案。例如,斐波那契函数的朴素递归实现的时间复杂度是O(2^n)
DP 解决方案只用O(n)
时间做同样的事情。
记忆化(自上而下的缓存填充)是指缓存和重用先前计算结果的技术。因此,memoizedfib
函数看起来像这样:
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]
}
您可以在此处阅读有关记忆和制表比较的更多信息。
您应该在这里掌握的主要思想是,由于我们的分而治之问题具有重叠的子问题,子问题解决方案的缓存成为可能,因此记忆/制表步入了现场。
由于我们现在熟悉 DP 先决条件及其方法,我们准备将上面提到的所有内容放在一张图片中。
如果您想查看代码示例,您可以在此处查看更详细的说明,其中您会找到两个算法示例:二分搜索和最小编辑距离(Levenshtein 距离),它们说明了 DP 和 DC 之间的区别。
分而治之和动态规划之间的另一个区别可能是:
分而治之:
动态规划:
有时在递归编程时,您多次调用具有相同参数的函数,这是不必要的。
著名的斐波那契数列示例:
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) 的示例。
我假设您已经阅读过 Wikipedia 和其他学术资源,所以我不会回收任何这些信息。我还必须警告说,我无论如何都不是计算机科学专家,但我会分享我对这些主题的理解的两分钱......
将问题分解为离散的子问题。斐波那契数列的递归算法是动态规划的一个例子,因为它通过首先求解 fib(n-1) 来求解 fib(n)。为了解决原来的问题,它解决了一个不同的问题。
这些算法通常会解决问题的相似部分,然后在最后将它们放在一起。合并排序是分而治之的经典例子。这个例子和斐波那契例子的主要区别在于,在归并排序中,分割可以(理论上)是任意的,无论你如何分割它,你仍然在归并和排序。无论您如何划分数组,都必须完成相同数量的工作来对数组进行归并排序。求解 fib(52)比求解 fib(2)需要更多的步骤。
我认为Divide & Conquer
这是一种递归方法和Dynamic Programming
表格填充。
例如,Merge Sort
是一种Divide & Conquer
算法,在每一步中,您将数组分成两半,递归调用Merge Sort
两半然后合并它们。
Knapsack
是一种Dynamic Programming
算法,因为您正在填写代表整个背包子问题的最佳解决方案的表格。表中的每个条目对应于在给定项目 1-j 的情况下,您可以在一袋重量 w 中携带的最大值。
分而治之在每个递归级别涉及三个步骤:
动态规划包括以下四个步骤:
1.表征最优解的结构。
2.递归定义最优解的值。
3.计算最优解的值。
4.根据计算信息构造最优解。
为了更容易理解,让我们将分而治之视为一种蛮力解决方案,并将其优化视为动态规划。
NB具有重叠子问题的分治算法只能使用 dp 进行优化。
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) 被重复。这就是斐波那契有重叠子问题的原因。
分而治之
动态规划