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