1. 程式人生 > >速懂edit distance(編輯距離)

速懂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公式如下:

編輯距離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