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

https://blog.csdn.net/bbbeoy/article/details/79613806

https://www.cnblogs.com/xiuyangleiasp/p/5023717.html

基本介绍

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

(像这种动态规划问题,要发散性思维,不能停留在固定思维上,多去设计动态转换方法,得出解决过程,不能太拘泥于过程的数值变化和数值变化)

简单例子

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

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

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

概念

  字符串的编辑距离,又称为Levenshtein距离,由俄罗斯的数学家Vladimir Levenshtein在1965年提出。是指利用字符操作,把字符串A转换成字符串B所需要的最少操作数。其中,字符操作包括:

  • 删除一个字符
  • 插入一个字符
  • 修改一个字符

  例如对于字符串"if"和"iff",可以通过插入一个'f'或者删除一个'f'来达到目的。

  一般来说,两个字符串的编辑距离越小,则它们越相似。如果两个字符串相等,则它们的编辑距离(为了方便,本文后续出现的“距离”,如果没有特别说明,则默认为“编辑距离”)为0(不需要任何操作)。不难分析出,两个字符串的编辑距离肯定不超过它们的最大长度(可以通过先把短串的每一位都修改成长串对应位置的字符,然后插入长串中的剩下字符)。

问题描述

  给定两个字符串A和B,求字符串A至少经过多少步字符操作变成字符串B。

问题分析

  1)首先考虑A串的第一个字符

  假设存在两个字符串A和B,他们的长度分别是lenA和lenB。首先考虑第一个字符,由于他们是一样的,所以只需要计算A[2...lenA]和B[2...lenB]之间的距离即可。那么如果两个字符串的第一个字符不一样怎么办?可以考虑把第一个字符变成一样的(这里假设从A串变成B串):

  • 修改A串的第一个字符成B串的第一个字符,之后仅需要计算A[2...lenA]和B[2...lenB]的距离即可;
  • 删除A串的第一个字符,之后仅需要计算A[2...lenA]和B[1...lenB]的距离即可;
  • 把B串的第一个字符插入到A串的第一个字符之前,之后仅需要计算A[1...lenA]和B[2...lenB]的距离即可。

  2)接下来考虑A串的第i个字符和B串的第j个字符。

  我们这个时候不考虑A的前i-1字符和B串的第j-1个字符。如果A串的第i个字符和B串的第j个字符相等,即A[i]=B[j],则只需要计算A[i...lenA]和B[j...lenB]之间的距离即可。如果不想等,则:

  • 修改A串的第i个字符成B串的第j个字符,之后仅需要计算A[i+1...lenA]和B[j+1...lenB]的距离即可;
  • 删除A串的第i个字符,之后仅需要计算A[i+1...lenA]和B[j...lenB]的距离即可;
  • 把B串的第j个字符插入到A串的第i个字符之前,之后仅需要计算A[i...lenA]和B[j+1...lenB]的距离即可。

  写到这里,自然会想到用递归求解或者动态规划求解,由于用递归会产生很多重复解,所以用动态规划。

构建动态规划方程

用edit[i][j]表示A串从第i个字符开始和B串从第j个字符开始的距离。则从上面的分析,不难推导出动态规划方程:

,其中

C++代码

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

if(a[i-1]==b[j-1])
                dp[i][j] = dp[i-1][j-1];

意思是如果值相等,

则dp[i][j]从A串从第i个字符开始 和 B串从第j个字符开始的距离 就等于从

dp[i-1][j-1]从A串从第 i-1个字符开始 和 B串从第 j-1个字符开始的距离

a b c d

a e f g

//dp[i-1][j-1] 修改A串的第i个字符成B串的第j个字符,之后仅需要计算A[i+1...lenA]和B[j+1...lenB]的距离即可;


//dp[i][j-1]   删除A串的第i个字符,之后仅需要计算A[i+1...lenA]和B[j...lenB]的距离即可;


//dp[i-1][j]   把B串的第j个字符插入到A串的第i个字符之前,之后仅需要计算A[i...lenA]和B[j+1...lenB]的距离即可。

#include <stdio.h>
#include <string.h>
#include <algorithm>
using std::min;
int lena, lenb;
char a[1010], b[1010];
void read()
{
    scanf("%s%s", a, b);
    lena = strlen(a);
    lenb = strlen(b);
}
 
int dp[1010][1010];
void work()
{
    for(int i=1; i<=lena; i++)//当一个数值为0时,距离就为另一个字符串的长度
        dp[i][0] = i;//用edit[i][j]表示A串从第i个字符开始和B串从第j个字符开始的距离
    for(int j=1; j<=lenb; j++)
        dp[0][j] = j;
    for(int i=1; i<=lena; i++)
        for(int j=1; j<=lenb; j++)
            if(a[i-1]==b[j-1])
                dp[i][j] = dp[i-1][j-1];
            else
                dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j]))+1;
    printf("%d\n", dp[lena][lenb]);
}

//dp[i-1][j-1] 修改A串的第i个字符成B串的第j个字符,之后仅需要计算A[i+1...lenA]和B[j+1...lenB]的距离即可;
//dp[i][j-1]   删除A串的第i个字符,之后仅需要计算A[i+1...lenA]和B[j...lenB]的距离即可;
//dp[i-1][j]   把B串的第j个字符插入到A串的第i个字符之前,之后仅需要计算A[i...lenA]和B[j+1...lenB]的距离即可。

int main()
{
    read();
    work();
    return 0;
}

几个小优化

  1. 如果满足A[i]==B[j](下标从1开始),实际上是可以直接取lev(i, j)=lev(i−1, j−1)的。因为此时字符相同是不需要任何编辑操作的。这一优化也可以从上文转移方程中构造不等关系得出。

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

 

 

 

k

i

t

t

e

n

 

 

0

1

2

3

4

5

6

 

0

0

1

2

3

4

5

6

s

1

1

1

2

3

4

5

6

i

2

2

1

2

3

4

5

6

t

3

3

3

2

1

2

3

4

t

4

4

 

 

 

 

 

 

i

5

5

 

 

 

 

 

 

n

6

6

 

 

 

 

 

 

g

7

7

 

 

 

 

 

 

 

dp[i]为上次留下的值,dp[i-1]为这次更新的值,

t1 = dp[0]++; //dp[i] i为lenb1到lenb2的距离,从str1到str2的距离,也就是第一列下的1到7的数值,同时到循环里面后会进

t1 = t2;//为下次准备,为下次的左上方的值,因为是一维滚动数组,数值发生变化,所以要提前保存

#include <stdio.h>
#include <string.h>
#include<iostream>
#include <algorithm>
using std::min;
using namespace std;
int lena, lenb;
char a[1010], b[1010];
void read()
{
    scanf("%s%s", a, b);
    lena = strlen(a);
    lenb = strlen(b);
}

int dp[1010];
void work()
{
    for(int j=1; j<=lenb; j++)
        dp[j] = j;
    int t1, t2;
    for(int i=1; i<=lena; i++)
    {                 //每进行一次运算,dp[0] 的距离就++;长度就加一
        t1 = dp[0]++; //dp[i] i为lenb1到lenb2的距离,从str1到str2的距离,
        //cout<<dp[0]<<endl;
        for(int j=1; j<=lenb; j++)
        {
            t2 = dp[j];
            if(a[i-1]==b[j-1])
                dp[j] = t1;
            else//dp[i]为上次留下的值,dp[i-1]为这次更新的值,
                dp[j] = min(t1, min(dp[j-1], dp[j]))+1;
            t1 = t2;//为下次准备,为下次的左上方的值
        }
    }
    printf("%d\n", dp[lenb]);
}


//dp[i-1][j-1] 修改A串的第i个字符成B串的第j个字符,之后仅需要计算A[i+1...lenA]和B[j+1...lenB]的距离即可;
//dp[i][j-1]   删除A串的第i个字符,之后仅需要计算A[i+1...lenA]和B[j...lenB]的距离即可;
//dp[i-1][j]   把B串的第j个字符插入到A串的第i个字符之前,之后仅需要计算A[i...lenA]和B[j+1...lenB]的距离即可。

int main()
{
    read();
    work();
    return 0;
}

猜你喜欢

转载自blog.csdn.net/while_black/article/details/89609697