转载:https://www.cnblogs.com/BlackStorm/p/5400809.html
基本介绍
Levenshtein距离是一种计算两个字符串间的差异程度的字符串度量(string metric)。我们可以认为Levenshtein距离就是从一个字符串修改到另一个字符串时,其中编辑单个字符(比如修改、插入、删除)所需要的最少次数。俄罗斯科学家Vladimir Levenshtein于1965年提出了这一概念。
简单例子
从字符串“kitten”修改为字符串“sitting”只需3次单字符编辑操作,如下:
-
- sitten ( k -> s )
- sittin ( e -> i )
- sitting ( _ -> g )
因此“kitten”和“sitting”的Levenshtein距离为3。
实现思想
如何编程实现这一算法呢?许多人试图用矩阵来解释,但实际上矩阵是最终可视化的工具,配合理解“为什么”比较方便,但从矩阵却比较难想到“怎么做”。
我们试图找到“从字符串A
A修改到字符串BB”这一问题的子解结构。当然反过来说“从字符串BB修改到字符串AA”和它是同一个问题,因为从AA中删掉一个字符来匹配BB,就相当于在BB中插入一个字符来匹配A
A,这两个操作是可以互相转化的。
假设字符序列A[1…i]
A[1…i]、B[1…j]B[1…j]分别是字符串AA、BB的前ii、jj个字符构成的子串,我们得到一个子问题是“从字符串A[1…i]A[1…i]修改到字符串B[1…j]B[1…j]”:⎡⎣⎢A:B:A[1]B[1]A[2]B[2]⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]A[i]B[j]⎤⎦⎥
⎣⎡A:B:A[1]B[1]A[2]B[2]⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]A[i]B[j]⎦⎤
① 插入操作:
-
- 当将A[1…i]
A[1…i]修改成B[1…j−1]B[1…j−1]需要操作数为op1op1,那么我插入一个字符A[i']=B[i]A[i′]=B[i]到A[i]A[i]和A[i+1]A[i+1]之间,用以匹配B[i]B[i],于是A[1…i]A[1…i]修改到B[1…j]B[1…j]所需操作数为op1+1op1+1。⎡⎣⎢⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]A[i]B[j]A[i′]ϕ⎤⎦⎥
-
- ⎣⎡⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]A[i]B[j]A[i′]ϕ⎦⎤
② 删除操作:
-
- 当将A[1…i−1]
A[1…i−1]修改成B[1…j]B[1…j]需要操作数为op2op2,那么我删掉字符A[i]A[i]也可以op2+1op2+1的操作数使两个子字符串匹配:⎡⎣⎢⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]ϕB[j]⎤⎦⎥
-
- ⎣⎡⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]ϕB[j]⎦⎤
③ 修改操作:
-
- 如果A[1…i−1]
A[1…i−1]修改成B[1…j−1]B[1…j−1]所需操作数为op3op3的话,我将字符A[i]A[i]替换成A[i']=B[j]A[i′]=B[j],就可以op3+1op3+1的操作数完成:⎡⎣⎢⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]A[i′]B[j]⎤⎦⎥
- ⎣⎡⋯⋯A[i−2]B[j−2]A[i−1]B[j−1]A[i′]B[j]⎦⎤
- 但如果此时字符A[i]==B[j]
- A[i]==B[j]的话,则不需要进行修改操作,操作数仍为op3
-
- op3。
综上所述,我们将字符串A[1…i]
A[1…i]修改成字符串B[1…j]B[1…j]所需操作为min{op1+1, op2+1, op3+1(ai≠bi)}min{op1+1, op2+1, op3+1(ai≠bi)},其中1(ai≠bi)1(ai≠bi)代表当ai≠biai≠bi时取值11,否则取值为0
0。
数学定义
数学上,我们定义两个字符串A
A和BB间的Levenshtein距离为levA, B(a, b)levA, B(a, b),其中aa、bb分别为字符串AA、BB的长度,而levA, B⎛⎝⎜⎜⎜⎜⎜⎜⎜i, j⎞⎠⎟⎟⎟⎟⎟⎟⎟=⎧⎩⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪ijmin⎧⎩⎨⎪⎪⎪⎪leva, b(i, j−1)+1leva, b(i−1, j)+1leva, b(i−1, j−1)+1(ai≠bi), j=0, i=0, otherwise
levA, B(i, j)=⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧ijmin⎩⎨⎧leva, b(i, j−1)+1leva, b(i−1, j)+1leva, b(i−1, j−1)+1(ai≠bi), j=0, i=0, otherwise
更多请参考 Wikipedia - Levenshtein_distance。
C++代码
有了状态转移方程,我们就可以愉快地DP了,时间复杂度O(MN)
O(MN),空间复杂度O(MN)
O(MN)。
1 #include <stdio.h> 2 #include <string.h> 3 #include <algorithm> 4 using std::min; 5 int lena, lenb; 6 char a[1010], b[1010]; 7 void read() { 8 scanf("%s%s", a, b); 9 lena = strlen(a); 10 lenb = strlen(b); 11 } 12 13 int dp[1010][1010]; 14 void work() { 15 for(int i=1; i<=lena; i++) dp[i][0] = i; 16 for(int j=1; j<=lenb; j++) dp[0][j] = j; 17 for(int i=1; i<=lena; i++) 18 for(int j=1; j<=lenb; j++) 19 if(a[i-1]==b[j-1]) 20 dp[i][j] = dp[i-1][j-1]; 21 else 22 dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j]))+1; 23 printf("%d\n", dp[lena][lenb]); 24 } 25 26 int main() { 27 read(); 28 work(); 29 return 0; 30 }
几个小优化
1. 如果满足A[i]==B[j]
A[i]==B[j](下标从11开始),实际上是可以直接取lev(i, j)=lev(i−1, j−1)
lev(i, j)=lev(i−1, j−1)的。因为此时字符相同是不需要任何编辑操作的。这一优化也可以从上文转移方程中构造不等关系得出。
2. 如果使用滚动数组,则空间复杂度可以降到O(2∗max{M, N})
O(2∗max{M, N})。但也可以通过保存lev(i−1, j−1)lev(i−1, j−1)来把空间复杂度降到O(max{M, N})
O(max{M, N}),如下:
以上即为Levenshtein距离算法的基本介绍1 int dp[1010]; 2 void work() { 3 for(int j=1; j<=lenb; j++) dp[j] = j; 4 int t1, t2; 5 for(int i=1; i<=lena; i++) { 6 t1 = dp[0]++; 7 for(int j=1; j<=lenb; j++) { 8 t2 = dp[j]; 9 if(a[i-1]==b[j-1]) 10 dp[j] = t1; 11 else 12 dp[j] = min(t1, min(dp[j-1], dp[j]))+1; 13 t1 = t2; 14 } 15 } 16 printf("%d\n", dp[lenb]); 17 }
-