速懂edit distance(編輯距離)
前言
今天看了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
核心部分為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