证明动态编程方法对最小编辑距离的正确性

时间:2016-03-07 03:55:35

标签: algorithm language-agnostic dynamic-programming

要计算最小编辑距离(将一个单词转换为另一个单词所需的最小插入,删除和替换),动态编程解决方案基于递归关系,其中检查两个字符串的最后一个字符。详细信息位于https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm

对于编辑距离,此算法的描述在因特网上无处不在,但所有这些都只是在没有证据的情况下断言其正确性。通过编辑距离的定义,您可以在中间插入,删除或替换字符,而不仅仅是在结尾处。那么你如何证明这种递归关系实际上存在呢?

3 个答案:

答案 0 :(得分:7)

使用归纳证明递归算法正确

首先,正如我在评论中所说,您可以将动态编程视为一种加速递归的方法,并且证明递归算法正确的最简单方法几乎总是induction:显示它&#39 ; s在一些小的基础情况下是正确的,然后表明,假设它对于大小为n的问题是正确的,它对于大小为n + 1的问题也是正确的。这种证明与递归结构密切相关。

这个问题的通常递归打破了问题"找到将字符串A编辑成字符串B&#34的最低成本;进入(| A | +1)(| B | +1)子问题"找到将字符串A的前i个字符编辑成字符串B"的前j个字符的最低成本,对于所有0< ; = i< = | A | 0< = j< = | B |。

选择基本案例

通常通过归纳,我们可以选择少量简单的基础案例(可能只有一个),表明我们可以轻松地为它们计算正确的答案,并且很明显所有其他案例的正确性将如何基本案例的正确性暗示了,因为不管我们从什么情况开始,都会有一个单一的"链"需要满足的假设,这个链条显然必须以我们的一个基本案例结束。然而对于这个特殊问题,为了证明算法最优地解决了(i,j)子问题,我们首先需要假设它解决了(i-1,j),(i,j-1)和(i-1) ,j-1)最佳子问题(因为如果这些子问题的任何答案不正确,它可以很容易地为(i,j)子问题计算完全错误的答案)。这将需要比平常更复杂的感应:而不是单一的"链"需要满足的假设,我们现在有一个分支&#34树"假设,每个点(最多)3个孩子。我们需要以这样的方式选择基本情况:对于任何(i,j),这整个假设树最终会停止",即它中的每个路径最终都会触及基本情况,其假设得到满足。

回顾一下:为了证明我们对(i,j)最优的解,我们必须假设我们有(i-1,j),(i,j-1)和(i-1,j-)的最优解。 1);为了满足(i-1,j)(即证明我们对(i-1,j)的解是最优的)的假设,我们需要假设我们有(i-2,j)的最优解。 ),(i-1,j-1)和(i-2,j-1)等等。如何选择有效的基础案例?在遍历这棵树的过程中,即从证明我们的解决方案到子问题(i,j)的过程中,正确地证明了我们对其任何一个孩子的解决方案。子问题(i',j')正确,我们注意到:

  1. i'中的至少一个<我或j' < j持有。
  2. 我们从不"跳过"子问题 - 也就是说,我们永远不会有i-i' > = 2,或j-j' > = 2。
  3. 基本上,如果我们在这棵树上走一步,至少我们的两个子问题协调中的一个" (i或j)减少,但从不超过1.这意味着如果我们继续在树上下行,那么无论哪个特殊的孩子"我们在下行的路上选择的子问题,我们必须最终打到一个子问题,其中一个坐标为(至少)一个0,即一个(i' 0)子问题,对于某些0< =我'' < = | A |或者(0,j'')子问题,对于某些0< = j'' < = | B |。这意味着如果我们将那些子问题作为我们的基本情况,我们就可以确保归纳树中的每个路径都能够满足其假设得到满足的基本情况,因此可以停止。

    幸运的是,这些基本案例确实很容易计算出最佳答案。考虑一个问题(i,0):这个问题要求将字符串A的前i个字符更改为字符串B的前0个字符所需的最低成本。显然,最好(仅!)的方法是删除所有我是A的前缀中的字符,代价为i。同样,问题(0,j)要求将A的前0个字符更改为B的前j个字符所需的最低成本:同样清楚,最好的方法是简单地在此前缀中插入所有j个字符B的成本,j。

    归纳步骤

    剩下的就是归纳步骤:假设我们已经计算了(i-1,j)的答案,我们正确地计算了(i,j)子问题的答案,(i,j -1)和(i-1,j-1)子问题正确。诀窍是看到以各种可能的方式将A的前i个字符编辑成B的前j个字符,实际上只有3个可能的事情我们可以用每个中的最后一个字符做这些前缀(即A中的第i个字符和B中的第j个字符):

    • 将A [i]与B [j]配对。它们匹配(成本0)或不匹配(成本1),但无论哪种方式,此配对的总成本必须是成本(0或1),加上编辑休息的最小可能成本 A(A [1 ... i-1])前缀加入B(B [1 .. j-1])前缀的 rest - 假设,我们已经正确计算了!
    • 删除A [i]。这需要1,因此执行此操作的总成本必须为1加上编辑A(A [1 .. i-1])前缀的 rest B的整个前缀(B [1 .. j]) - 假设我们已经正确计算了!
    • 插入B [j]。这需要1,因此执行此操作的总成本必须为1加上将整个前缀A(A [1 .. i])编辑到 rest中的最小可能成本 B的前缀(B [1 .. j-1]) - 假设我们已经正确计算了!

    由于这3件事情是我们可以做的唯一可能的事情,并且我们已经计算了做这件事的整体最低成本,所以总体上最好的事情必须是其中3个中最好的。这证明我们正确计算将A的前i个字符编辑为B的前j个字符所需的最低成本,对于任何 i和j - 所以特别是,它是真的对于i = | A |和j = | B |,即用于将完整的字符串A编辑成完整的字符串B。

答案 1 :(得分:0)

我找不到任何令人满意的证据,所以我做了一个。我读过的所有证据实际上并不能证明这些案例是详尽无遗的。

(A) 总是存在至少一个最佳的编辑系列,称之为Eo

这是微不足道的。

(B)Eo 中,有些字符永远不会被插入或更改。将最后一个这样的字符称为pivot

如果没有,我们可以使用字符串的开头作为枢轴。在 Eo 中,这个公共子序列从头到尾永远不会改变。我们假设它是最长公共子序列或任何东西。

eg) #KITTEN, # SITTING → 枢轴:'#ITTN '[-1] = 'N'

这个枢轴有一些属性可以让问题变得更容易。

  1. 枢轴的左侧和右侧彼此独立。一侧发生的任何编辑都不会影响另一侧。
  2. 根据(B)的定义,目标字符串主元右侧的所有字符都应该通过替换或添加来正确。

由于(1)我们只需要考虑原始字符串和目标字符串的主元的右侧。让我们假设左侧是最优的(贪婪),并且编辑次数将被添加到总数中。
例如)
#weofanbmcdepmqu -> #eopasbc tdewni
只考虑pmqu → wni
使用(2)这个子问题可以解决如下。
len(原始)>len(目标): asdfgh→qwer
删除 len(original)-len(target) 次以适应长度,并替换 len(target) 次以适应字符。它可以按任何顺序完成,因为它们在距离上都是等价的。在最后一次编辑时删除最后一个字符是这样的解决方案之一,它等于 dp(original[:-1]→target) + 1
len(原始) asdf→qwerty
添加 len(target)-len(original) 次以适应长度,并替换 len(original) 次以适应字符。在最后一次编辑时添加最后一个字符等于 dp(original→target[:-1])+1
len(original)==len(target)!=0: asdf→qwer
更换长度。它等于在最后一次编辑时替换最后一个字符。 dp(原始[:-1]→目标[:-1])+1
len(original)==len(target)==0: ' '→' '
最后一个字符是枢轴。当最后一个字符是相同的字符时会发生这种情况。您不编辑枢轴,因此它与 dp(original[:-1]->target[:-1])

相同

答案 2 :(得分:0)

这是我的证明:

为了从字符串 A 到 B,我们从左到右执行一组最佳操作。有4种操作:EQ(保留字符)、SWAP(改变字符)、INS(插入字符)、DEL(删除字符)。这是编辑距离的定义,EQ的代价==0。

定义A的长度为a,B的长度为b。

定义 d[a,b] 为 A 的前 a 个字符和 B 的前 b 个字符之间的编辑距离,这表示为了到达 B 需要对 A 进行的操作(除 EQ 外)的次数.

如果我们看一系列最佳操作(必须有一个),最后一个操作有 4 个选项。我们不知道最后一个操作,所以我们检查了所有 4 个选项并选择了最好的一个(最好的意思是最小编辑距离)。

如果最后一个操作是 EQ,则意味着 A[a]==B[b] 并且编辑距离等于 d[a-1, b-1],因为 EQ 不被视为“编辑距离” ”成本。

如果最后一个操作是 SWAP,则意味着 A[a]!=B[b] 并且编辑距离等于 d[a-1, b-1] + 1。

如果操作是 INS 表示编辑距离是 d[a, b-1] + INS(to B) 如果操作是DEL,则表示编辑距离为d[a-1, b] + DEL(from A)

我们只是尝试从 LAST 到 FIRST 的 4 种操作的所有组合,直到找到最佳路径。实际上每一步都是3个操作,因为我们可以根据当前字符是否相等来决定是否检查EQ或SWAP。 (如果它们相等,则无需检查 SWAP 操作,反之亦然)。

既然我们尝试了所有可能的操作,那么递归公式一定是正确的。