最近在研究一些算法,因为之前自己的编程都没有经过系统的学习,所以通过学一些算法和数据结构来提高编程水平。
关于字符串的编辑距离算法,网上的文章有很多,内容大同小异,都是用动态规划来做,而且给出了实现的代码。但是,对于算法中的状态转换方程 d[i, j] = min(d1,d2,d3) = min(d[i-1, j]+1, d[i, j-1]+1, d[i-1, j-1] + (A[i] == A[j] ? 0 : 1)) 的推导,说明都不太详细或者基于直觉,少有较为完整的证明。像知乎上就有人问(https://www.zhihu.com/question/23139644):
题主在理解状态转移方程的时候,一直过不去。
假设字符串A(长为n), B(长为m),0 < i <= n, 0 < j <= m,D(i, j)代表A[1:i]变为B[1:j]的编辑距离,编辑距离指由A=>B的最短操作数,每个操作都只针对一个字符。
那么 D(i,j) = min(D(i - 1, j - 1) + (A[i] == B[j]) ? 0 : 1, D(i - 1, j) + 1, D(i, j - 1) + 1)
如何用数学、严谨、形式化的方式去证明上述递推公式成立?而不是想当然、由题而知、显然可得?
下文以我自己的理解,结合图片,来推导状态转换方程。
问题描述
字符串的编辑距离是指,字符串A修改为字符串B所需要的最少的编辑次数。插入、删除、修改都算作一次修改。
解法推导
现在研究在老字符串到新字符串的编辑过程中,老字符串的 原有字符 的 变化 情况。可能的变化有:
- 被修改;
- 被删除;
- 位置移动(由插入新字符/删除字符导致)。
举个例子,例如从“abcdefg" -> “aebcdhf” 的一种编辑,原有字符的变化情况如下:
图中,“e”被修改,“g”被删除,“b” “c” “d” “e(后改为h)” “f” 的位置发生了变化。
值得注意的是,由于插入、修改和删除不涉及字符顺序的变化,所以原字符串中的字符的顺序在新字符串中不会发生变化。例如,若将 "ab" 修改为 “ba”,则视为两次修改: a 修改为 b,b 修改为 a,因为并没有规定“交换字符顺序”的编辑方式。
设源字符串为A,目标字符串为B;字符串 A 有 i 个字符;字符串 B 有 j 个字符。
现在考虑在某编辑中,对字符串 A 的 最后一个字符 A[i] 的处理情况。
情况有三种:
- 最后一个字符 被删除。此时的情况比较简单,只需要将字符串A剩下的字符串 A[0...i-1] 修改为字符串B即可(设最少编辑 k1次)。此时的最少编辑次数为 k1 + 1。
- 最后一个字符 未被删除,但是其在新字符串中 是最后一个字符。此时,需要将原字符串剩下的子串 A[1...i-1] 修改成为新字符串的子串 B[1...j-1](如下图所示)(设最少编辑 k2 次)。如果 A[i] == B[j], 则最后一个字符无需修改,最少编辑次数为 k2; 否则,需要修改最后一个字符,最少编辑次数为 k2 + 1。
- 最后一个字符 未被删除,但是其在新字符串中 不是最后一个字符。因为在编辑过程中,字符串的字符顺序不会变化,所以原字符串的前 i - 1 个字符在新字符串的位置必在 A[i] 的新位置之前(若未被删除)(如下图所示)。这样的编辑等价于将字符串 A 修改为字符串 B 的子串 B[1...j-1](设最少编辑 k3 次),然后再在尾部添加字符串 B 的最后一个字符即可。此时的最少编辑次数 k3 + 1。
由于这3种情况 涵盖了对最后一个字符的所有处理方式,所以,最少的编辑次数一定会从这三种情况中产生。设 d[i, j] 为字符串 A 的子串 A[1...i] 到 字符串B的子串 B[1...j] 的最短编辑距离,则:
- 情况1: d1 = d[i-1, j] + 1;
- 情况2: 当 A[i] = B[j] 时,d2 = d[i - 1, j - 1]; 否则,d2 = d[i - 1, j - 1] + 1;
- 情况3: d3 = d[i, j - 1] + 1。
因此,d[i, j] = min(d1,d2,d3) = min(d[i-1, j]+1, d[i, j-1]+1, d[i-1, j-1]+t) ,其中 t = 0(A[i] = B[j]),t = 1(A[i] != B[j])。
知道了状态转换方程,由动态规划来求解该问题的具体方法就不赘述了,需要注意的是初始条件的选取:
- d[0, 0] = 0;
- 当 i = 0 时,d[0, j] = j;(将空字符串修改为长度为 j 的字符串需要增添 j 次)
- 当 j = 0 时,d[i, 0] = i。(将长度为 i 的字符串修改为空需要删除 i 次)