問題描述:
對於序列S和T, 它們之間的距離定義為: 對二者其一進行幾次以下操作: 1, 刪除一個字符; 2, 插入一個字符; 3, 改變一個字符. 每進行一次操作, 計數增加1. 將S和T變為相等序列的最小計數就是兩者的編輯距離(edit distance)或者叫相似度. 請給出相應算法及其實現.
分析:
假設序列S和T的長度分別為m和n, 兩者的編輯距離表示為edit[m][n]. 則對序列進行操作時存在以下幾種情況:
- a, 當S和T的末尾字符相等時, 對末尾字符不需要進行上述定義操作中(亦即"編輯")的任何一個, 也就是不需要增加計數. 則滿足條件: edit[m][n] = edit[m - 1][n - 1].
- b, 當S和T的末尾字符不相等時, 則需要對兩者之一的末尾進行編輯, 相應的計數會增加1.
- b1, 對S或T的末尾進行修改, 以使之與T或S相等, 則此時edit[m][n] = edit[m - 1][n - 1] + 1;
- b2, 刪除S末尾的元素, 使S與T相等, 則此時edit[m][n] = edit[m - 1][n] + 1;
- b3, 刪除T末尾的元素, 使T與S相等, 則此時edit[m][n] = edit[m][n - 1] + 1;
- b4, 在S的末尾添加T的尾元素, 使S和T相等, 則此時S的長度變為m+1, 但是此時S和T的末尾元素已經相等, 只需要比較S的前m個元素與T的前n-1個元素, 所以滿足edit[m][n] = edit[m][n - 1] + 1;
- b5, 在T的末尾添加S的尾元素, 使T和S相等, 此時的情況跟b4相同, 滿足edit[m][n] = edit[m - 1][n] + 1;
- c, 比較特殊的情況是, 當S為空時, edit[0][n] = n; 而當T為空時, edit[m][0] = m; 這個很好理解, 例如對於序列""和"abc", 則兩者的最少操作為3, 即序列""進行3次插入操作, 或者序列"abc"進行3次刪除操作.
所以, 以上我們不難推出編輯距離的動態規划方程為:
, 其中
所以, 字符串編輯距離的動態規划算法的遞歸實現可以用如下的Java代碼表示:
1 public static int editDistance(String a, String b) { 2 if (a == null || b == null) { 3 return -1; 4 } 5 return editDistance(a, a.length() - 1, b, b.length() - 1); 6 } 7 8 public static int editDistance(String a, int m, String b, int n) { 9 if (m < 0 || n < 0) { 10 return 1; 11 } else if (a.charAt(m) == b.charAt(n)) { 12 return editDistance(a, m - 1, b, n - 1); 13 } else { 14 return Math.min(Math.min(editDistance(a, m - 1, b, n) + 1, editDistance(a, m, b, n - 1) + 1), editDistance(a, m - 1, b, n - 1) + 1); 15 } 16 }
UPDATE:
同時, 由編輯距離的動態規划方程我們可以看出, edit[m][n]可以由edit[m - 1][n - 1], edit[m - 1][n], edit[m][n - 1]得出, 而如果edit是一個二維數組的話, edit[m][n]可以由它的上, 左, 左上三個位置的元素通過條件判斷得出. 亦即我們可以通過遍歷二維數組, 然后通過回溯來計算當前值.
例如對於字符串S = "sailn"和T = "failing", 對二維數組進行初始化為:
| m\n | f | a | i | l | i | n | g | |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
| s | 1 | 1 | ||||||
| a | 2 | |||||||
| i | 3 | |||||||
| l | 4 | |||||||
| n | 5 |
因為S[0] = s, T[0] = f, 則S[0] != T[0], 則對應於上述二維矩陣, edit[1][1] = min(edit[0][0], edit[0][1], edit[1][0]) + 1即edit[1][1] = min(0, 1, 1) + 1即edit[1][1] = 0 + 1 = 1.
| m\n | f | a | i | l | i | n | g | |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
| s | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| a | 2 | 2 | 1 | |||||
| i | 3 | |||||||
| l | 4 | |||||||
| n | 5 |
而對於S[1] = a, T[1] = a, S[1] = T[1], 則對應於二維矩陣, edit[2][2] = edit[1][1], 所以edit[2][2] = 1. 所以按照這種規則, 將上述二維矩陣填滿則如下:
| m\n | f | a | i | l | i | n | g | |
| 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[m][n] = edit[5][7] = 3.
所以, 按照上述思路即動態規划的回溯解法的Java版本可以如下進行:
1 public static int editDistance(String a, String b) { 2 if (a == null || b == null) { 3 return -1; 4 } 5 int[][] matrix = new int[a.length() + 1][b.length() + 1]; 6 for (int i = 0; i < a.length() + 1; i++) { 7 for (int j = 0; j < b.length() + 1; j++) { 8 if (i == 0) { 9 matrix[i][j] = j; 10 } else if (j == 0) { 11 matrix[i][j] = i; 12 } else { 13 if (a.charAt(i - 1) == b.charAt(j - 1)) { 14 matrix[i][j] = matrix[i - 1][j - 1]; 15 } else { 16 matrix[i][j] = 1 + Math.min(Math.min(matrix[i - 1][j], matrix[i][j - 1]), matrix[i - 1][j - 1]); 17 } 18 } 19 } 20 } 21 return matrix[a.length()][b.length()]; 22 }
