前言
今天看了Stanford编辑距离代码,感觉写得不错,写一篇博客记录下。
编辑距离的定义是:从字符串A到字符串B,中间需要的最少操作权重。这里的操作权重一般是:
- 删除一个字符(deletion)
- 插入一个字符(insertion)
- 替换一个字符(substitution)
- 他们的权重都是1
编辑距离的算法一般用dp。很多博客写到这里就结束了,因此十分晦涩难懂。因为没有对其加主谓语,完全就是耍流氓。正确的说法应该是:
- 删除A末尾一个字符(deletion)
- 用B末尾插入A末尾一个字符(insertion)
- 把A末尾字符替换成B末尾的一个字符(substitution)
为什么?
算法及实现
我们举一个实际例子
- 长度为m的字符串A,len(A) = m
- 长度为n的字符串B,len(B) = n
则A到B的编辑距离dp公式如下:
先不要急着看懂,我慢慢解释。
Q2: 为什么d是一个[m+1][n+1]大小的二维数组,为什么d数组要比字符串长度大一?
A2: 考虑A、B都为空字符串,我们还是需要一个[1][1]大小的数组记录其编辑距离为0。更进一步也就是说,我们假设字符串A为"AC",则我们需要考虑['', 'A', 'AC']三种情况。
Q1: 如何理解d[i][j]的计算公式?
-
A1: 第(i,j)个位置的计算需要依赖于和它相邻的三个元素(i-1,j)、(i, j-1)和(i-1,j-1),关键是哪一个对应删除,哪一个对应于插入,哪一个对应于替换?如果此时A[i]不等于B[j],则(下面为全文最重要部分):
- 对于(i-1,j-1)时,d(i-1, j-1)表示完成从A[0,i-1]到B[0,j-1]的编辑次数,即现在A[0,i-1]=B[0,j-1],对于(i,j),我们直接把A[i]替换成B[j]即完成编辑。因此(i-1,j-1)对应于把A[i]用B[j]替换的一次操作
- 对于(i-1, j)时,d(i-1, j)表示完成从A[0, i-1]到B[0, j]的编辑次数,即现在A[0,i-1]=B[0,j],对于(i,j),我们直接把A[i]删除即可完成编辑,因此(i-1,j)对应于把A[i]删除的一次操作
- 对于(i, j-1)时,d(i, j-1)表示完成从A[0, i]到B[0, j-1]的编辑次数,即现在A[0,i]=B[0,j-1],对于(i,j),我们直接用B[j]插入到A[i]的位置即可完成编辑,因此(i,j-1)对应于把B[j]插到A[i]的一次操作
理解了上面的文字就理解编辑距离DP算法了,写得有点冗长。
这里给一个带Damerau–Levenshteindistance距离的代码,其中添加了一种操作:
- 置换两个字符(transposition),也就是说'ab'到'ba'的操作消耗值为1
代码地址:https://gist.github.com/nlpjoe
核心部分为score_edit_distance(self, source, target)
:
def score_edit_distance(self, source, target):
if source == target:
return 0
s_pos = len(source)
t_pos = len(target)
self.clear(s_pos, t_pos)
for i in range(s_pos + 1):
for j in range(t_pos + 1):
b_score = self.score[i][j]
if b_score != self.worse():
continue
if i == 0 and j == 0: # 0,0位置为空,默认为正确
b_score = self.best()
else:
if i > 0: # 删除权重
b_score = min(b_score, self.score[i-1][j] + self.delete_cost(source[i-1]))
if j > 0: # 插入权重
b_score = min(b_score, self.score[i][j-1] + self.insert_cost(target[j-1]))
if i > 0 and j > 0: # 替换权重
b_score = min(b_score, self.score[i-1][j-1] + self.substitute_cost(source[i-1], target[j-1]))
if i > 1 and j > 1: # 置换权重
b_score = min(b_score, self.score[i-2][j-2] + self.transpose_cost(source[i-2], source[i-1], target[j-2], target[j-1]))
self.score[i][j] = b_score
return self.score[s_pos][t_pos]
输出结果为:
0
5.0
1.0