编辑距离在自然语言处理中经常用到,例如,在英文拼写纠错中,对于一个错误单词,需要通过编辑距离找到对应的相似的候选词。
介绍
编辑距离(Levenshtein distance):
计算一个字符串转成另一个字符串所使用的最少操作数
有如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
例如:
将word1 = "horse"转换为word2 = “ros”,所使用的最少操作数为3.
- horse -> rorse (将 ‘h’ 替换为 ‘r’)
- rorse -> rose (删除 ‘r’)
- rose -> ros (删除 ‘e’)
将word2转成word1的最少操作数也为3.
因为上述的三个操作都有对应的逆操作,rorse -> rose (删除 ‘r’) 对应于 rose -> rorse (插入’r’)
代码
使用动态规划的的方法来解决该问题
求解 x[:i]
与y[:j]
之间的编辑距离
(1) 若x[i-1] = y[j-1]
: 则 x[:i]
与y[:j]
之间的编辑距离等于 x[:i-1]
与y[:j-1]
之间的编辑距离
(2)否则,x[:i]
与y[:j]
之间的编辑距离等于以下三种情况的最小值
- 插入:
x[:i-1]
与y[:j]
之间的编辑距离 + 1 - 删除:
x[:i]
与y[:j-1]
之间的编辑距离 + 1 - 替换:
x[:i-1]
与y[:j-1]
之间的编辑距离 + 1
递推公式如下:
d p [ i , j ] = { d p [ i − 1 , j − 1 ] 若 i , j ≥ 0 , x i − 1 = y j − 1 min { d p [ i , j − 1 ] , d p [ i − 1 , j ] , d p [ i − 1 , j − 1 ] } + 1 若 i , j ≥ 0 , x i − 1 ≠ y j − 1 dp[i, j]= \begin{cases} dp[i-1, j-1] & \text { 若 } i, j\geq0, x_{i-1}=y_{j-1} \\ \min\{dp[i, j-1], dp[i-1, j], dp[i-1, j-1]\} +1 & \text { 若 } i, j\geq0, x_{i-1} \neq y_{j-1}\end{cases} dp[i,j]={dp[i−1,j−1]min{dp[i,j−1],dp[i−1,j],dp[i−1,j−1]}+1 若 i,j≥0,xi−1=yj−1 若 i,j≥0,xi−1=yj−1
具体代码如下:
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m = len(word1)
n = len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# dp[i][j]表示word1[:i]与word2[:j]之间的编辑距离
# 若一个字符串为空,编辑距离等于另一个字符串的长度
for i in range(1, m + 1):
dp[i][0] = i
for j in range(1, n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
return dp[m][n]
obj = Solution()
word1 = "horse"
word2 = "ros"
res = obj.minDistance(word1, word2)
print(res)
# 3
dp矩阵如下:
r | o | s | ||
---|---|---|---|---|
0 | 1 | 2 | 3 | |
h | 1 | 1 | 2 | 3 |
o | 2 | 2 | 1 | 2 |
r | 3 | 2 | 2 | 2 |
s | 4 | 3 | 3 | 2 |
e | 5 | 4 | 4 | 3 |
根据dp矩阵,从后往前还回溯,得到结果:
代码如下:
def backpath(self, word1, word2, dp):
i = len(dp) - 1
j = len(dp[0]) - 1
res = []
while i > 0 or j > 0:
a = dp[i - 1][j - 1] if i > 0 and j > 0 else float("inf")
b = dp[i - 1][j] if i > 0 else float("inf")
c = dp[i][j - 1] if j > 0 else float("inf")
min_val = min([a, b, c])
if dp[i][j] == a and a == min_val:
i -= 1
j -= 1
# 没有操作
elif a == min([a, b, c]):
# 通过替换来的
i -= 1
j -= 1
res.append((i, i + 1, word1[i], word2[j], "sub"))
elif b == min([a, b, c]):
i = i - 1
res.append((i, i + 1, word1[i], "", "del"))
else:
j = j - 1
res.append((i + 1, i + 1, "", word2[j], "ins"))
print(res)
return res
输出
[(4, 5, 'e', '', 'del'), (2, 3, 'r', '', 'del'), (0, 1, 'h', 'r', 'sub')]
有时候,还需要考虑,加上操作交换相邻两个字符
的情况,这种就更加复杂,称为 Damerau–Levenshtein distance
https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance