字符串编辑距离(Levenshtein距离)算法

基本介绍

  Levenshtein距离是一种计算两个字符串间的差异程度的字符串度量(string metric)。我们可以认为Levenshtein距离就是从一个字符串修改到另一个字符串时,其中编辑单个字符(比如修改、插入、删除)所需要的最少次数。俄罗斯科学家Vladimir Levenshtein于1965年提出了这一概念。

 

简单例子

  从字符串“kitten”修改为字符串“sitting”只需3次单字符编辑操作,如下:

    • sitten ( k -> s )
    • sittin ( e -> i )
    • sitting ( _ -> g )

  因此“kitten”和“sitting”的Levenshtein距离为3。

 

实现思想

  如何编程实现这一算法呢?许多人试图用矩阵来解释,但实际上矩阵是最终可视化的工具,配合理解“为什么”比较方便,但从矩阵却比较难想到“怎么做”。

  我们试图找到“从字符串AAA修改到字符串BBB”这一问题的子解结构。当然反过来说“从字符串BBB修改到字符串AAA”和它是同一个问题,因为从AAA中删掉一个字符来匹配BBB,就相当于在BBB中插入一个字符来匹配AAA,这两个操作是可以互相转化的。

  假设字符序列A[1i]A[1\ldots i]A[1i]B[1j]B[1\ldots j]B[1j]分别是字符串AAABBB的前iiijjj个字符构成的子串,我们得到一个子问题是“从字符串A[1i]A[1\ldots i]A[1i]修改到字符串B[1j]B[1\ldots j]B[1j]”:A:B:A[1]B[1]A[2]B[2]A[i2]B[j2]A[i1]B[j1]A[i]B[j]\left[\begin{matrix}\begin{aligned}&A:&&A[1]&&A[2]&&\cdots&&A[i-2]&&A[i-1]&&A[i]\\\\&B:&&B[1]&&B[2]&&\cdots&&B[j-2]&&B[j-1]&&B[j]\end{aligned}\end{matrix}\right]A:B:A[1]B[1]A[2]B[2]A[i2]B[j2]A[i1]B[j1]A[i]B[j]

  ① 插入操作

    • 当将A[1i]A[1\ldots i]A[1i]修改成B[1j1]B[1\ldots j-1]B[1j1]需要操作数为op1op_1op1,那么我插入一个字符A[i']=B[i]A[i']=B[i]A[i]=B[i]A[i]A[i]A[i]A[i+1]A[i+1]A[i+1]之间,用以匹配B[i]B[i]B[i],于是A[1i]A[1\ldots i]A[1i]修改到B[1j]B[1\ldots j]B[1j]所需操作数为op1+1op_1+1op1+1A[i2]B[j2]A[i1]B[j1]A[i]B[j]A[i]ϕ\left[\begin{matrix}\begin{aligned}&&\cdots&&\color{Red}{A[i-2]}&&\color{Red}{A[i-1]}&&\mathbf{\color{Red}{A[i]}}&&\mathbf{\color{Blue}{A[i']}}&&\\\\&&\cdots&&\color{Red}{B[j-2]}&&\mathbf{\color{Red}{B[j-1]}}&&\mathbf{\color{Blue}{B[j]}}&&\phi&&\end{aligned}\end{matrix}\right]A[i2]B[j2]A[i1]B[j1]A[i]B[j]A[i]ϕ

  ② 删除操作

    • 当将A[1i1]A[1\ldots i-1]A[1i1]修改成B[1j]B[1\ldots j]B[1j]需要操作数为op2op_2op2,那么我删掉字符A[i]A[i]A[i]也可以op2+1op_2+1op2+1的操作数使两个子字符串匹配:A[i2]B[j2]A[i1]B[j1]ϕB[j]\left[\begin{matrix}\begin{aligned}&&\cdots&&\color{Red}{A[i-2]}&&\mathbf{\color{Red}{A[i-1]}}&&\mathbf{\color{Blue}{\phi}}&&\\\\&&\cdots&&\color{Red}{B[j-2]}&&\color{Red}{B[j-1]}&&\mathbf{\color{Red}{B[j]}}&&\end{aligned}\end{matrix}\right]A[i2]B[j2]A[i1]B[j1]ϕB[j]

  ③ 修改操作

    • 如果A[1i1]A[1\ldots i-1]A[1i1]修改成B[1j1]B[1\ldots j-1]B[1j1]所需操作数为op3op_3op3的话,我将字符A[i]A[i]A[i]替换成A[i']=B[j]A[i']=B[j]A[i]=B[j],就可以op3+1op_3+1op3+1的操作数完成:A[i2]B[j2]A[i1]B[j1]A[i]B[j]\left[\begin{matrix}\begin{aligned}&&\cdots&&\color{Red}{A[i-2]}&&\mathbf{\color{Red}{A[i-1]}}&&\mathbf{\color{Blue}{A[i']}}&&\\\\&&\cdots&&\color{Red}{B[j-2]}&&\mathbf{\color{Red}{B[j-1]}}&&\mathbf{\color{Blue}{B[j]}}&&\end{aligned}\end{matrix}\right]A[i2]B[j2]A[i1]B[j1]A[i]B[j]
    • 但如果此时字符A[i]==B[j]A[i]==B[j]A[i]==B[j]的话,则不需要进行修改操作,操作数仍为op3op_3op3

  综上所述,我们将字符串A[1i]A[1\ldots i]A[1i]修改成字符串B[1j]B[1\ldots j]B[1j]所需操作为min{op1+1, op2+1, op3+1(aibi)}min\{op_1+1,\ op_2+1,\ op_3+1_{(a_i\neq b_i)}\}min{op1+1, op2+1, op3+1(aibi)},其中1(aibi)1_{(a_i\neq b_i)}1(aibi)代表当aibia_i\neq b_iaibi时取值111,否则取值为000

 

数学定义

  数学上,我们定义两个字符串AAABBB间的Levenshtein距离为levA, B(a, b)lev_{A,\ B}(a,\ b)levA, B(a, b),其中aaabbb分别为字符串AAABBB的长度,而levA, Bi, j=ijminleva, b(i, j1)+1leva, b(i1, j)+1leva, b(i1, j1)+1(aibi), j=0, i=0, otherwiselev_{A,\ B}(i,\ j)=\left\{\begin{matrix}\begin{aligned}&i&&,\ j=0\\&j&&,\ i=0\\&min\left\{\begin{matrix}lev_{a,\ b}(i,\ j-1)+1\\lev_{a,\ b}(i-1,\ j)+1\\lev_{a,\ b}(i-1,\ j-1)+1_{(a_i\neq b_i)}\end{matrix}\right.&&,\ otherwise\end{aligned}\end{matrix}\right.levA, B(i, j)=ijminleva, b(i, j1)+1leva, b(i1, j)+1leva, b(i1, j1)+1(aibi), j=0, i=0, otherwise

  更多请参考 Wikipedia - Levenshtein_distance。

 

 

C++代码

  有了状态转移方程,我们就可以愉快地DP了,时间复杂度O(MN)O(MN)O(MN),空间复杂度O(MN)O(MN)O(MN)

复制代码
 1 #include 
 2 #include <string.h>
 3 #include 
 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]A[i]==B[j](下标从111开始),实际上是可以直接取lev(i, j)=lev(i1, j1)lev(i,\ j)=lev(i-1,\ j-1)lev(i, j)=lev(i1, j1)的。因为此时字符相同是不需要任何编辑操作的。这一优化也可以从上文转移方程中构造不等关系得出。

  2. 如果使用滚动数组,则空间复杂度可以降到O(2max{M, N})O(2*max\{M,\ N\})O(2max{M, N})。但也可以通过保存lev(i1, j1)lev(i-1,\ j-1)lev(i1, j1)来把空间复杂度降到O(max{M, N})O(max\{M,\ N\})O(max{M, N}),如下:

复制代码
 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 }
复制代码

 

以上即为Levenshtein距离算法的基本介绍,如果您喜欢,请点个推荐吧~如果您有宝贵意见,欢迎在下方评论区提出哦~

 

你可能感兴趣的:(Nature,language,Programming)