动态规划求编辑距离
位于分类 自然语言处理
这两天在写一个简单的单词拼写检查器(Spell checker),本来求编辑距离只是其中的一个子问题,现在把它罗列出来,是因为鉴于看到一些书,把本来不是很难的问题讲得很复杂,而且不知道是不是一些作者,为了显得自己水平之高,几乎没有任何的推导,只有一堆结果罗列。当然这些都是题外话了,但是,书者,传道授业解惑也,若是使读者迷惑不能自拔,不知是读者愚钝,还是书之不书。
现在我们开始切入正题。先来说说什么是编辑距离,编辑距离是一种字符串之间相似程度的计算方法。按照Damerau给出的定义,即两个字符串之间的编辑距离等于使一个字符串变成另外一个字符串而进行的(1)插入、(2)删除、(3)替换或(4)相邻字符交换位置而进行操作的最少次数。用ed来表示编辑距离。
比如:ed("recoginze", "recognize") == 1(需要交换两个相邻字符"i"和"n"的位置)
ed("sailn", "failing") == 3(需要将"s"换成"f",在字母"l"后边插入"i","n"后面插入"g")。
有的时候,变换并不唯一,重要的是要求出这一些变换路径中最短的数量。
关于编辑距离的求法,普遍的采用的是动态规划方法。但是,目前网上的资料中提及的编辑路径并不准确,缺少了第(4)步的处理。详细的同学们可以查阅这两篇文章——【串和序列处理 2】字符串编辑距离算法(Java)和动态规划求编辑距离——算法解题报告(C++)。
下面给出维基上对动态规划的定义:
动态规划是一种在数学和计算机科学中使用的,用于求解包含重叠子问题的最优化问题的方法。其基本思想是,将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。
其实就是把一个复杂的最优解问题分解成一系列较为简单的最优解问题,再将较为简单的最优解问题一步步分解,直到能够一眼看出为止。
我们拿"sailn"和"failing"这两个字符串作例子。首先我们定义这样一个函数——edit(i, j),它表示字符串1的长度为i的子串到字符串2的长度为j的子串的编辑距离。
首先我们作出初始化edit(0, j) = j(字符串1子串长度为0,字符串2子串有多少个字符,就作多少次增加操作;于是同理,edit(i, 0) = i。)
这里,我们要注意到对于操作(4),即交换相邻字符串的操作,我们要把某个字符通过这个操作到另一个位置上,我们最多只能执行一次操作,即只能移动到邻位上。原因是什么呢?这是因为,移动两次的话,就没有优势了,它的操作等于两次替换操作的操作数。大于2次的时候,移动操作会更差。所以,我们要进行操作(4),最多只发生一次操作。
我们可以得出这样一段动态规划公式:
- 如果i == 0 且 j == 0,edit(i, j) = 0
- 如果i == 0 且 j > 0,edit(i, j) = j
- 如果i > 0 且j == 0,edit(i, j) = i(2、3点之前已经陈述)
- 如果0 < i ≤ 1 且 0 < j ≤ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) },这里当字符串1的第i个字符不等于字符串2的第j个字符时,f(i, j) = 1;否则,f(i, j) = 0。
- 如果i > 1且 j > 1时,这个时候可能出现操作(4),由之前的推导,我们只能交换一次,否则就没有意义。这个时候在比较最小值中可能加入edit(i-2, j-2) +1,什么时候加入呢?假设i-2长度的字符串1子串和j-2长度的字符串2子串已经得出最优解,这个时候如果s1[i-1] == s2[j] 并且s1[i] == s2[j-1],这时就在比较值中加入edit(i-2, j-2) + 1(这个1是交换一次的操作)
我们来把这个过程演绎一遍。我们首先给出这样一个矩阵:
0 f a i l i n g 0 s a i l n 在经过初始化之后,矩阵变成这样:
0 f a i l i n g 0 0 1 2 3 4 5 6 7 s 1 a 2 i 3 l 4 n 5 现在要计算edit(1, 1),而edit(0, 1) + 1 == 2,edit(1, 0) + 1 == 2,edit(0, 0) + f(1, 1) == 0 + 1 == 1,他们其中最小的为1,因此edit(1, 1) == 1。按照此法计算,直到计算到edit(2, 2),矩阵为这样:
0 f a i l i n g 0 0 1 2 3 4 5 6 7 s 1 1 2 3 4 5 6 7 a 2 2 i 3 l 4 n 5 这个时候,edit(2, 1) + 1 == 3,edit(1, 2) + 1 == 3,edit(1, 1) + f(2, 2) == 1 + 0 == 1,而此时s1[2] == 'a' 而 s2[1] == 'f'‘,不满足条件,所以,交换相邻字符的操作不纳入比较最小数中计算。接着往下计算,得出最后矩阵为:
0 f a i l i n g 0 0 1 2 3 4 5 6 7 s 1 1 2 3 4 5 6 7 a 2 2 1 2 3 4 5 6 i 3 3 2 1 2 3 4 5 l 4 4 3 2 1 2 3 4 n 5 5 4 3 2 2 2 3 可以看到edit(len(s1), len(s2)) == 3,验证了先前的结论。
接下来就给出Python实现的代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445#!/usr/bin/env python
# -*- coding: utf-8 -*-
def
ed(s1, s2):
'''
>>> ed('eeba', 'abac')
3
>>> ed('abc', 'cba')
2
>>> ed('cbc', 'eba')
2
>>> ed('recoginze', 'recognize')
1
>>> ed('sailn', 'failing')
3
>>> ed('ab', 'ba')
1
'''
# 动态规划求编辑距离
# param s1: 字符串1
# param s2: 字符串2
len1
=
len
(s1)
len2
=
len
(s2)
# 初始化矩阵
matrix
=
[[i
+
j
for
j
in
range
(len2
+
1
)]
for
i
in
range
(len1
+
1
)]
for
row
in
range
(len1):
for
col
in
range
(len2):
comp
=
[matrix[row
+
1
][col]
+
1
, matrix[row][col
+
1
]
+
1
]
if
s1[row]
=
=
s2[col]:
comp.append(matrix[row][col])
else
:
comp.append(matrix[row][col]
+
1
)
# 对相邻字符交换位置的处理判断
if
row >
0
and
col >
0
:
if
s1[row]
=
=
s2[col
-
1
]
and
s1[row
-
1
]
=
=
s2[col]:
comp.append(matrix[row
-
1
][col
-
1
]
+
1
)
matrix[row
+
1
][col
+
1
]
=
min
(comp)
return
matrix[len1][len2]
重要的是这段代码:
123if
row >
0
and
col >
0
:
if
s1[row]
=
=
s2[col
-
1
]
and
s1[row
-
1
]
=
=
s2[col]:
comp.append(matrix[row
-
1
][col
-
1
]
+
1
)
同学们要用其他语言实现,只需要实现以上判断,来进行操作(4)。
注意到ed函数的docstring******现了类似命令行的句子,这是为了方便进行doctest测试。要测试全部数据,只需加上以下几句话:
123if
__name__
=
=
'__main__'
:
import
doctest
doctest.testmod()
四月23
11条留言
2011年 十一月21日 10:28 p.m.
你好
最后的
if row > 0 and col > 0:
if s1[row] == s2[col-1] and s1[row-1] == s2[col]:
comp.append(matrix[row-1][col-1]+1)
是不是应该改为
if row > 0 and col > 0:
if s1[row] == s2[col-1] and s1[row-1] == s2[col]:
comp.append(matrix[row-2][col-2]+1)