前言
今天看了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公式如下:
編輯距離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