Fork me on GitHub

动态规划 001 - 编辑距离(Levenshtein Distance)问题

问题

编辑距离(Levenshtein Distance)问题

字符串的编辑距离也被称为距Levenshtein距离(Levenshtein Distance),属于经典算法,常用方法使用递归,更好的方法是使用动态规划算法,以避免出现重叠子问题的反复计算,减少系统开销。


思考

也许我们以前遇过这样一个问题:

计算两个字符串的相似度。

关于相似度的定义,从下面这个例子了解一下:

比如,对于”abcdefg”和”abcdef”两个字符串来说,我们认为可以通过增加/减少一个”g”的方式来达到目的。把这个操作所需要的次数定义为两个字符串的距离,而相似度等于”距离+1”的倒数。也就是说,”abcdefg”和”abcdef”的距离为1,相似度 为1/2=0.5。给定任意两个字符串,你是否能写出一个算法来计算它们的相似度呢?(其实这个问题的关键是要求两个字符串的编辑距离。)

这个问题其实是由俄罗斯科学家Vladimir Levenshtein在1965年提出的。


分析

*先考虑一些特殊情况:

d(null, B) = strlen(B);
d(A, null) = strlen(A);
d(A, B) = 0 当且仅当 A = B.

*再考虑一般情况下的 d(A, B)

运用动态规划求出递归方程,将原问题分解为若干个子问题进行求最优解,而后得出原问题的最优解,采用“填表的方法”。
设计步骤:对每个子问题只求解一次,将其结果保存在一张表(构造一个行数为n+1 列数为 m+1 的矩阵 , 用来保存完成某个转换需要执行的最少操作的次数, 其中,n为字符串A的长度,m为字符串B的长度)中。

对矩阵中的一点d[i][j],保存从A[0:i]变到B[0:j]的编辑距离。其中,这里S[0:i]变到t[0:j]有三种情况,求得这三种情况的最小值作为最小操作数:

(1)设可以在k1个操作内将s[0:i-1]转换为t[0:j],用k1+1次操作将s[0:i]转化为t[0:j],只需要先在“s[0:i]转化为t[0:j]”的操作开端做1次移除操作移除s[i]将s[0:i]转化为s[0:i-1],然后再做k1个操作就可以转换为t[0:j]。对应表格,对应矩阵d[i][j]处即填入k1+1。(左)


(2)设可以在k2个操作内将s[0:i]转换为t[0:j-1],用k2+1次操作将s[0:i]转化为t[0:j],先用k2次操作将s[0:i]转化为t[0:j-1],然后再执行1次插入操作在“s[0:i]变成t[0:j-1]的操作”的末尾插入“增加t[j]”的一次操作,即可将s[0:i]转化为t[0:j]。对应矩阵d[i][j]处即填入k2+1。(上)


(3)设可以在k3个操作内将s[0:i-1]转化为t[0:j-1] ,此时需分情况讨论:
**若s[i]==t[j],S[0:i]变到t[0:j]就只要k3个操作,对应矩阵d[i][j]处即填入k3;
**若s[i]!=t[j],则需1次换操作加在s[0:i-1]转化为t[0:j-1]的操作数基础上就可以将S[0:i]变到t[0:j],共k3+1次。对应矩阵d[i][j]处即填入k3+1。(左上角)

其实,通过上面三种情况的分析,我们可以得到下面这个递推公式:

递推公式


实现过程

下面我们用一个简单的例子来实现下算法的过程,以abc和abe这两个字符串为例

  1. 首先进行如下初始化(开辟d[n+1][m+1]数据空间,相应位置数据初始化):
    1

    (ps:”A处”是一个标记,只是为了方便讲解,不是这个表的内容。)

  2. 计算A处的值

    它的值取决于:左边的1、上边的1、左上角的0.
    按照上面我们的分析:
    上面的值和左面的值都要求加1,这样得到1+1=2。
    A处 由于是两个a相同,左上角的值加0.这样得到0+0=0。
    这是后有三个值,左边的计算后为2,上边的计算后为2,左上角的计算为0,所以A处 取他们里面最小的0.

  3. 于是表成为下面的样子
    2

    在B处 会同样得到三个值,左边计算后为3,上边计算后为1,在B处 由于对应的字符为a、b,不相等,所以左上角应该在当前值的基础上加1,这样得到1+1=2,在(3,1,2)中选出最小的为B处的值。

  4. 于是表就更新了
    3

    C处 计算后:上面的值为2,左边的值为4,左上角的:a和e不相同,所以加1,即2+1,左上角的为3。在(2,4,3)中取最小的为C处 的值。

  5. 依次推得到
    这里写图片描述

    I处: 表示abc 和abe 有1个需要编辑的操作。这个是需要计算出来的。
    同时,也获得一些额外的信息。
    A处: 表示a 和a 需要有0个操作。字符串一样
    B处: 表示ab 和a 需要有1个操作。
    C处: 表示abe 和a 需要有2个操作。
    D处: 表示a 和ab 需要有1个操作。
    E处: 表示ab 和ab 需要有0个操作。字符串一样
    F处: 表示abe 和ab 需要有1个操作。
    G处: 表示a 和abc 需要有2个操作。
    H处: 表示ab 和abc 需要有1个操作。
    I处: 表示abe 和abc 需要有1个操作。


程序实现

#include<bits/stdc++.h>
using namespace std;

int min(int a,int b,int c)
{
    return (a<b)?(a<c?a:c):(b<c?b:c);
}
int main()
{
    FILE * file;
    freopen("input.txt", "r", stdin);
    freopen("output.txt","w",stdout);

    int d[6][8];
    string a,b;
    cin>>a>>b;

    for (int i=0;i<=b.size()+1;i++)
    {
        for (int j=0; j<=a.size()+1;j++)
        {
            if (i==0&&j==0)
                d[i][j]=0;
            else if (i==0&& j > 0)
                d[i][j]=j;
            else if (i>0&&j==0)
                d[i][j]=i;

            else if(i>=1&&j>=1)
            {
                int k =((b[i-1]==a[j-1])?0:1);
                d[i][j]=min(d[i-1][j]+1, d[i][j-1]+1,d[i-1][j-1]+k);
            }
        }
    }
/*
    for(int i=0;i<b.size()+1;i++)
    {
        for(int j=0;j<a.size()+1;j++)
        {
            cout<<d[i][j]+" ";
        }
        cout<<endl;
    }
*/
    cout<<d[b.size()+1][a.size()+1];
    return 0;
}

更多

https://my.oschina.net/mustang/blog/58125
https://blog.csdn.net/the_k1/article/details/78442814
https://www.cnblogs.com/cangT-Tlan/p/6219005.html
http://www.cnblogs.com/jiabei521/p/3353390.html
http://wdhdmx.iteye.com/blog/1343856





【附:一文一图】

桂林

 

posted @ 2018-04-09 00:22  zhouie  阅读(375)  评论(0编辑  收藏  举报