在本文中,我正试图解释动态编程和划分之间的差异/相似性,并根据两个示例征服方法:二进制搜索和最小编辑距离(Levenshtein距离)。
此外,在JavaScript文章中调整大小的内容感知图像中,我通过了接缝雕算法的动态规划的另一个强大而简单的例子。你也可能想要检查一下。
当我开始学习算法时,我很难了解动态编程(DP)的主要思想以及它与剥夺和征服(DC)方法不同。当它比较比较这两个范式时,通常斐波纳契函数随着救援而言。但是,当我们尝试使用两个DP和DC方法来解释它们的同样的问题时,对我来说感觉可能会失去有助于更快地捕捉差异的宝贵细节。这些细节告诉我们,每个技术都适用于不同类型的问题。
我仍然在理解DP和DC差异的过程中,我不能说我到目前为止已经完全掌握了这个概念。但我希望这篇文章揭示了一些额外的光线,并帮助您完成其他有价值的算法范例作为动态编程和剥夺和征服的另一步。
正如我所看到的,现在我可以说动态编程是鸿沟的延伸和征服范例。
我不会把它们视为完全不同的东西。因为它们都通过递归地将问题分解为相同或相关类型的两个或更多个子问题,直到这些变得足够简单,直接解决。然后将子问题的解决方案组合以给出原始问题的解决方案。
那么为什么我们仍然有不同的范式名称,那么为什么我称为动态编程一个扩展。这是因为只有当问题有一定的限制或先决条件时,才能对问题应用于问题。在动态编程之后,用备忘或制表技术延伸划分和征服方法。
正如我们刚才发现的两个关键属性,除以划分和征服问题必须有效,以便动态编程适用:
重叠的子问题 - 问题可以分解为若干次重复使用的子问题,或者问题的递归算法遍历和过度地求于相同的子问题而不是始终生成新的子问题
一旦满足这两个条件,我们可以说可以使用动态编程方法来解决这种分割和征服问题。
动态编程方法以两种技术(备忘录和制表)扩展划分和征服方法,其两种技术都具有存储和重复使用可能急剧提高性能的子问题解决方案。例如,Fibonacci函数的Naive递归实现具有O(2 ^ n)的时间复杂度,其中DP解决方案仅与O(n)时间相同。
备忘(自上而下的缓存填充)是指高速缓存和重用先前计算结果的技术。因此,铭刻的FIB函数如下所示:
memfib(n){if(mem [n]未定义)如果(n< 2)结果= n结果= memfib(n-2)+ memfib(n-1)mem [n] =结果返回mem [n] ]}
制表(自下而上缓存填充)是相似的,但侧重于填充缓存的条目。计算缓存中的值是最简单的。表格版本的fib会如下所示:
tabfib(n){mem [0] = 0 mem [1] = 1对于i = 2 ... n mem [i] = mem [i-2] + mem [i-1] return mem [n]}
这里应该掌握的主要思想是,因为我们的分割和征服问题具有重叠的子问题,所以子问题解决方案的缓存变得可能,因此备忘录/制表率上升到场景上。
由于我们现在熟悉DP先决条件及其准备将上面提到的一张图片所提到的所有方法。
动态编程和划分和征服范例依赖,让我们去尝试使用DP和DC方法解决一些问题,使这个例证更加清晰。
二进制搜索算法,也称为半间隔搜索,是一个搜索算法,其在排序阵列中找到目标值的位置。二进制搜索将目标值与数组的中间元素进行比较;如果它们不平等,则消除目标无法撒谎的一半,并且搜索在剩下的一半继续,直到找到目标值。如果搜索以剩余的一半为空而结束,则目标不在数组中。
以下是二进制搜索算法的可视化,其中4是目标值。
二进制搜索算法逻辑二进制搜索算法决策树你可以清楚地看到这里分裂并征服解决问题的原则。我们迭代地将原始数组分解为子阵列,并尝试在那里找到所需的元素。
我们可以将动态编程应用于它吗?不,它是因为没有重叠的子问题。每次我们将阵列拆分为完全独立的部分。根据分裂并征服先决条件/限制,子问题必须以某种方式重叠。
通常每次绘制决策树时,它实际上是一棵树(而不是一个决定图),它意味着您没有重叠的子问题,这不是动态编程问题。
在这里,您可以使用测试用例和解释找到二进制搜索功能的完整源代码。
函数binarysearch(sortedArray,Sentilement){Let StartIndex = 0; Let EndIndex = SortedArray .Length-1;而(startIndex< = endindex){const middleindex = startIndex + Math。地板((EndIndex - StartIndex)/ 2); //如果我们发现该元素只是返回其位置。 if(sortedArray [middenIndex] === sentilement){return middenindex; } //决定选择哪一半:左或右。 if(SortedArray [MiddenIndex]< SentiveLement){//转到阵列的右半部分。 startIndex = middenIndex + 1; } else {//转到阵列的左半部分。 EndIndex = MiddenIndex - 1; }}返回 - 1; }
通常,当涉及动态编程示例时,默认情况下拍摄FibonAcci编号算法。但是,让我们花一点复杂的算法来拥有某种品种,应该帮助我们掌握这个概念。
最小编辑距离(或Levenshtein距离)是用于测量两个序列之间的差异的字符串度量。非正式地,两个单词之间的Levenshtein距离是将一个单词更改为另一个单词所需的最小单字符编辑(插入,删除或替换)的最小数量。
例如,“小猫”和“坐姿”之间的Levenshtein距离是3,因为以下三个编辑将一个人变成另一个,并且没有少于三个编辑:
这具有广泛的应用,例如拼写检查,用于光学字符识别,模糊字符串搜索和软件的校正系统,以帮助基于翻译记忆库的自然语言翻译。
在数学上,通过功能Lev(| A |,|()给出两个弦A,B(分别的长度)之间的Levenshtein距离(|,|,|,其中:
注意,最小值中的第一元件对应于删除(从A到B),第二到插入和第三个以匹配或不匹配,具体取决于各个符号是否相同。
好的,让我们试着弄清楚这个公式正在谈论的内容。让我们采取一个简单的例子,即找到字符串我和我的最小编辑距离。直观地您已经知道此处的最小编辑距离为1操作,此操作是“用Y替换e”。但是,让我们尝试以算法形式形式形式形式,以便能够做出更复杂的例子,例如星期六进入星期天。
要将公式应用于M e→M y转换,我们需要知道先前的ME→M,M→MY和M→M→M→M→M→M→M→M→M→M的最小距离。然后我们需要选择最小值并添加+1操作以转换最后一个字母e→y。
因此,我们可以在此处看到解决方案的递归性:ME的最小编辑距离→即我的转换是根据三个先前可能的转换计算的。因此,我们可以说这是分割和征服算法。
查找ME和STRINGS单元之间的最小编辑距离(0,1)的简单示例包含红色编号1.这意味着我们需要1个操作来将M转换为空字符串:删除M.这就是此数字为红色的原因。
单元格(0,2)包含红色。这意味着我们需要2个操作来将我转换为空字符串:删除e,删除m。
单元格(1,0)包含绿色数字1.这意味着我们需要1个操作来将空字符串转换为M:插入M.这就是为什么此数字为绿色。
单元格(2,0)包含绿色数字2.这意味着我们需要2个操作来将空字符串转换为我的:插入Y,插入M.
单元格(1,1)包含数字0.这意味着它不需要将M转换为M.
单元格(1,2)包含红色。这意味着我们需要1个操作来将我转换为M:删除E.
这对于如我们的小矩阵(仅为3x3),这看起来很容易。但是如何计算更大矩阵的所有数字(让我们说9x7,星期六→星期日转换)?
好消息是,根据公式,您只需要三个相邻的单元(I-1,J),(I-1,J-1),(I,J-1)来计算当前单元格的数量(i j)。我们需要做的就是找到至少这三个单元格的最少,然后在I-S行和J-S列中有不同的字母,添加+1
最小编辑距离问题的递归性质可以确定我们刚刚在这里处理鸿沟和征服问题。但我们可以应用动态编程方法吗?这个问题是否满足了我们重叠的子问题和最佳的子结构限制?是的。让我们从决策图中看到它。
决策图最小编辑距离与重叠子问题首先,这不是决策树。这是一个决策图。您可能会在标记为红色的图片上看到许多重叠的子问题。而且,没有办法减少操作的数量,并使其从公式中少于那些三个相邻的细胞。
此外,您可能会注意到矩阵中的每个单元号都是基于先前计算的。因此,在此处应用标记技术(填充自下而上方向上的高速缓存)。您将在下面的代码示例中看到它。
进一步应用这些原则我们可以解决周六→星期日转型的更复杂的案例。
最小编辑距离将在此处转换为周日,您可以在此处找到具有测试用例和解释的最小编辑距离功能的完整源代码。
功能levenshteinkistance(a,b){const distancematrix = array(b .length + 1)。填充(null)。映射(()=>阵列(a .length + 1)。填充(null)); for(让我= 0; i< = a .length; i + = 1){distancematrix [0] [i] = i; for(让j = 0; j&l lt;长度; j + = 1){distancematrix [j] [0] = j; for(让J = 1; j&l lt;长度; j + = 1){for(让我= 1; i< = a .length; i + = 1){const指示符= a [i - 1] === B [J - 1]? 0:1; distancematrix [j] [i] =数学。 min(distancematrix [j] [i-1] + 1,//删除distancematrix [j - 1] [i] + 1,//插入distancematrix [j - 1] [i-1] +指示符,//替换) ; }}返回distancematrix [b .length] [a .length]; }
在本文中,我们比较了两种算法方法,如动态编程和分割和征服。我们发现动态编程基于除法和征服原理,并且只有在问题具有重叠的子问题和最佳子结构(如Levenshtein距离外壳)时,才能应用。然后动态编程是使用备忘或制表技术来存储重叠的子问题的解决方案以供以后使用。
我希望这篇文章没有给你带来更多的混乱,而是在这两个重要的算法概念上阐明一些光线! :) 您可以在JavaScript算法和数据结构存储库中找到更多的分割和征服和动态编程问题的更多示例。