您的位置:首页 > 其它

编辑距离与最长公共子序列总结

2015-12-04 16:27 204 查看
前言:

其实编辑距离和最长公共子序列是对同一个问题的描述,都能够显示出两个字符串之间的“相似度”,即它们的雷同程度。而子序列与字串的区别在于字串是连续的,子序列可以不连续,只要下标以此递增就行。

 

编辑距离:

Problem description:

  设A 和B 是2 个字符串。要用最少的字符操作将字符串A 转换为字符串B。这里所说的字符操作包括 (1)删除一个字符; (2)插入一个字符; (3)将一个字符改为另一个字符。将字符串A变换为字符串B 所用的最少字符操作数称为字符串A到B 的编辑距离,记为 d(A,B)。试设计一个有效算法,对任给的2 个字符串A和B,计算出它们的编辑距离d(A,B)。

Input

  输入的第一行是字符串A,文件的第二行是字符串B。

Output

  程序运行结束时,将编辑距离d(A,B)输出。

Sample Input

fxpimu

xwrs

Sample Output

5

#include<stdio.h>

#include <stdlib.h>

#include <string.h>

int _Min(int a,int b,int c)

{

int min=a;

if (b <min)

min=b;

if(c <min)

min=c;

return min;

}

int ComputeDistance(char s[],char t[])

{

int n = strlen(s);

int m = strlen(t);

//int d[][] = new int[n + 1, m + 1]; // matrix

int **d = (int **)malloc((n+1) * sizeof(int *));   //如何用malloc返回值强制转化为二重指针

for(int i=0; i<=n; ++i)

{

d[i] = (int *)malloc((m+1) * sizeof(int));

}

// Step 1

if (n == 0)

{

return m;

}

if (m == 0)

{

return n;

}

// Step 2

for (int i = 0; i <= n; i++)

{

d[i][0] =i;

}

for (int j = 0; j <= m; d[0][j] = j++)

{

d[0][j] =j;

}

// Step 3

for (int i = 1; i <= n; i++)

{

//Step 4

for (int j = 1; j <= m; j++)

{

// Step 5

int cost = (t[j-1] == s[i-1]) ? 0 : 1;

// Step 6

d[i][j] = _Min(d[i-1][j]+1, d[i][j-1]+1,d[i-1][j-1]+cost);

}

}

// Step 7

return d
[m];

}

int main(int argc, char *argv[])

{

char a[9999];

char b[9999];

printf("请输入字符串1\n");

scanf("%s",&a);

printf("请输入字符串2\n");

scanf("%s",&b);

int result= ComputeDistance(a,b);

printf("%d\n",result);

system("PAUSE");

return 0;

}

////////////////////

Refrence :        Dynamic Programming Algorithm(DPA) for Edit-Distance
编辑距离

       关于两个字符串s1,s2的差别,可以通过计算他们的最小编辑距离来决定。

       所谓的编辑距离: 让s1和s2变成相同字符串需要下面操作的最小次数。

1.         把某个字符ch1变成ch2

2.         删除某个字符

3.         插入某个字符
例如     s1 =
“12433”和s2=”1233”;

                     则可以通过在s2中间插入4得到12433与s1一致。

                    即 d(s1,s2) = 1 (进行了一次插入操作)
编辑距离的性质
计算两个字符串s1+ch1, s2+ch2的编辑距离有这样的性质:

1.         d(s1,””) = d(“”,s1) =|s1|   d(“ch1”,”ch2”)
=ch1 == ch2 ? 0 : 1;

2.         d(s1+ch1,s2+ch2) = min(     d(s1,s2)+ ch1==ch2 ? 0 : 1 ,

d(s1+ch1,s2),

d(s1,s2+ch2)  );

              第一个性质是显然的。

              第二个性质:         由于我们定义的三个操作来作为编辑距离的一种衡量方法。

                                         于是对ch1,ch2可能的操作只有

1.         把ch1变成ch2

2.         s1+ch1后删除ch1             d =(1+d(s1,s2+ch2))

3.         s1+ch1后插入ch2             d =(1 + d(s1+ch1,s2))

                                         对于2和3的操作可以等价于:

                                         _2.   s2+ch2后添加ch1             d=(1+d(s1,s2+ch2))

                                         _3.   s2+ch2后删除ch2             d=(1+d(s1+ch1,s2))

                     因此可以得到计算编辑距离的性质2。
复杂度分析
从上面性质2可以看出计算过程呈现这样的一种结构(假设各个层用当前计算的串长度标记,并假设两个串长度都为
n )
可以看到,该问题的复杂度为指数级别 3的 n次方,对于较长的串,时间上是无法让人忍受的。

       分析:    在上面的结构中,我们发现多次出现了(n-1,n-1),
(n-1,n-2)……。换句话说该结构具有重叠子问题。再加上前面性质2所具有的最优子结构。符合动态规划算法基本要素。因此可以使用动态规划算法把复杂度降低到多项式级别。
动态规划求解

       首先为了避免重复计算子问题,添加两个辅助数组。
一.    保存子问题结果。

M[ |s1| ,|s2| ] , 其中M[ i , j ]表示子串 s1(0->i)与
s2(0->j)的编辑距离
二.    保存字符之间的编辑距离.

E[ |s1|, |s2| ] , 其中 E[ i, j ] = s[i] = s[j] ?0 : 1
三.  新的计算表达式
根据性质1得到

M[ 0,0] = 0;

M[ s1i, 0 ] = |s1i|;

M[ 0, s2j ] = |s2j|;
根据性质2得到

M[ i, j ]   = min(     m[i-1,j-1] + E[ i, j ] ,

                            m[i, j-1] ,

                            m[i-1,j]  );

       复杂度

              从新的计算式看出,计算过程为

              i=1 -> |s1|

                     j=1 -> |s2|

                            M[i][j] =
……

              因此复杂度为 O( |s1| * |s2| ),如果假设他们的长度都为n,则复杂度为
O(n^2)
 

解题代码:

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

int fun(char sa[],char sb[])

{

       intlen_a=strlen(sa),len_b=strlen(sb);

       chararry[100][100]={0};

       inti,j;

       inta,b,c,t;

       for(i=0;i<=len_a;i++)

       {

              for(j=0;j<=len_b;j++)

              {

                     if(i==0)arry[i][j]=j;

                     elseif(j==0)arry[i][j]=i;

                     else

                     {

                            a=arry[i-1][j]+1;

                            b=arry[i][j-1]+1;

                            if(sa[i-1]==sb[j-1])c=arry[i-1][j-1];

                            else c=arry[i-1][j-1]+1;

                            t=b<c?b:c;

                            arry[i][j]=a<t?a:t;

                     };

              }

       }

  return arry[i-1][j-1];

}

int main()

{

       intline,i;

       intans[100];

   char sa[10000],sb[10000],e;

       scanf("%d",&line);

   e=getchar();

   for(i=0;i<line;i++)

       {

              scanf("%s",sa);

              scanf("%s",sb);

              ans[i]=fun(sa,sb);

       }

       for(i=0;i<line;i++)printf("%d\n",ans[i]);

       return0;

}

 

 

解题思路:

利用动态规划的方法。建立一个arry[len_a][len_b]的二维数组,行数和列数皆从0开始,行数n,列数m分别代表字符串a的前n个字符,和字符串b的前m个字符,arry
[m]代表字符串a的前n个字符和字符串b的前m个字符之间的编辑距离。首先初始化二维数组的第一行和第一列,分别为方格所在列数和行数,让后按如下方法初始化每一个方格。

 

arry[i][j]=min{arry[i-1][j]+1,arry[i][j-1]+1,arry[i-1][j-1]+sa[i]!=sb[j]}

整体用公式表达:


 

编辑距离的应用:
DNA分析
拼字检查
语音辨识
抄袭侦测
相似度计算
 
解题方法的改进:DNA分析 http://poj.org/problem?id=3356 题目描述:
    脱氧核糖核酸即常说的DNA,是一类带有遗传信息的生物大分子。它由4种主要的脱氧核苷酸(dAMP、dGMP、dCMT和dTMP)通过磷酸二酯键连接而成。这4种核苷酸可以分别记为:A、G、C、T。
 
    DNA携带的遗传信息可以用形如:AGGTCGACTCCA.... 的串来表示。DNA在转录复制的过程中可能会发生随机的偏差,这才最终造就了生物的多样性。
 
    为了简化问题,我们假设,DNA在复制的时候可能出现的偏差是(理论上,对每个碱基被复制时,都可能出现偏差):
 
  1. 漏掉某个脱氧核苷酸。例如把 AGGT 复制成为:AGT
 
    2. 错码,例如把 AGGT 复制成了:AGCT
 
    3. 重码,例如把 AGGT 复制成了:AAGGT
 
 
    如果某DNA串a,最少要经过 n 次出错,才能变为DNA串b,则称这两个DNA串的距离为 n。
 
    例如:AGGTCATATTCC 与 CGGTCATATTC 的距离为 2
 
    你的任务是:编写程序,找到两个DNA串的距离。
 
 
【输入、输出格式要求】
 
    用户先输入整数n(n<100),表示接下来有2n行数据。
 
    接下来输入的2n行每2行表示一组要比对的DNA。(每行数据长度<10000)
 
    程序则输出n行,表示这n组DNA的距离。
 
    例如:用户输入:
3
AGCTAAGGCCTT
AGCTAAGGCCT
AGCTAAGGCCTT
AGGCTAAGGCCTT
AGCTAAGGCCTT
AGCTTAAGGCTT
 
    则程序应输出:
1
1
2
 
 
【注意】
 
    请仔细调试!您的程序只有能运行出正确结果的时候才有机会得分!
   
    在评卷时使用的输入数据与试卷中给出的实例数据可能是不同的。
 
    请把所有函数写在同一个文件中,调试好后,拷贝到【考生文件夹】下对应题号的“解答.txt”中即可。
   
    相关的工程文件不要拷入。
   
    源代码中不能使用诸如绘图、Win32API、中断调用、硬件操作或与操作系统相关的API。
   
    允许使用STL类库,但不能使用MFC或ATL等非ANSI c++标准的类库。
 
    例如,不能使用CString类型(属于MFC类库),不能使用randomize, random函数(不属于ANSI C++标准)
 
结题代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int fun(charsa[],char sb[])
{
       int len_a=strlen(sa),len_b=strlen(sb);
       char arry[10000][10000];
      
       int i,j;
       int a,b,c,t;
       for(i=0;i<=len_a;i++)
       {
              for(j=0;j<=len_b;j++)
              {
                     if(i==0)arry[i][j]=j;
                     else if(j==0)arry[i][j]=i;
                     else
                     {
                            a=arry[i-1][j]+1;
                            b=arry[i][j-1]+1;
                            if(sa[i-1]==sb[j-1])c=arry[i-1][j-1];
                            elsec=arry[i-1][j-1]+1;
                            t=b<c?b:c;
                            arry[i][j]=a<t?a:t;
                     };
              }
       }
   return arry[i-1][j-1];
}
int main()
{
       int line,i;
       int ans[100];
    char sa[10000],sb[10000],e;
       scanf("%d",&line);
    e=getchar();
    for(i=0;i<line;i++)
       {
              scanf("%s",sa);
              scanf("%s",sb);
              ans[i]=fun(sa,sb);
       }
       for(i=0;i<line;i++)printf("%d\n",ans[i]);
       return 0;
}
 
解题总结:
1.我好不容易把这个程序编好了,然后又好不容易才发现int arry【10000】【10000】数组不能定义,估计占用空间太大,如果定义chararry[1000][1000]程序运行成功。
2.在定义变量的时候(尤其是指针,数组变量)首先给它赋一个初始值,以防在接下来的程序中没有赋值但是却引用了。
3.改进:可以让arry[10000][10000]动态的用arry[2][10000]生成,因为问题的本质是得到arry[10000][10000]元素就行了,并且根据每个元素生成的原理只需要两行就行了。
 
最长公共子序列:
问题描述:
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
   
考虑最长公共子序列问题如何分解成子问题,设A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,并Z=“z0,z1,…,zk-1”为它们的最长公共子序列。不难证明有以下性质:
(1) 如果am-1=bn-1,则zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列;
(2) 如果am-1!=bn-1,则若zk-1!=am-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列;
(3) 如果am-1!=bn-1,则若zk-1!=bn-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列。
这样,在找A和B的公共子序列时,如有am-1=bn-1,则进一步解决一个子问题,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一个最长公共子序列;如果am-1!=bn-1,则要解决两个子问题,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列,再取两者中较长者作为A和B的最长公共子序列。
 
 
求解:
引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定搜索的方向。
我们是自底向上进行递推计算,那么在计算c[i,j]之前,c[i-1][j-1],c[i-1][j]与c[i][j-1]均已计算出来。此时我们根据X[i] = Y[j]还是X[i] != Y[j],就可以计算出c[i][j]。
问题的递归式写成:



回溯输出最长公共子序列过程:



 
算法分析:
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i
= 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m
+ n)。
 
 
代码:
 
#include <stdio.h>
#include <string.h>
#define MAXLEN 100

void LCSLength(char *x,char
*y,int m,int n,int
c[][MAXLEN],int b[][MAXLEN])
...{
    int i, j;
   
    for(i = 0; i<= m; i++)
       c[i][0] = 0;
    for(j = 1; j<= n; j++)
       c[0][j] = 0;
    for(i = 1;i<= m; i++)
    ...{
        for(j = 1; j<= n; j++)
        ...{
           if(x[i-1] == y[j-1])
           ...{
               c[i][j] = c[i-1][j-1] + 1;
               b[i][j] = 0;
           }
           elseif(c[i-1][j] >= c[i][j-1])
           ...{
               c[i][j] = c[i-1][j];
               b[i][j] = 1;
            }
           else
           ...{
               c[i][j] = c[i][j-1];
               b[i][j] = -1;
           }
        }
    }
}

void PrintLCS(int b[][MAXLEN],char
*x,int i,int j)
...{
    if(i == 0 ||j == 0)
        return;
    if(b[i][j]== 0)
    ...{
       PrintLCS(b, x, i-1, j-1);
       printf("%c ", x[i-1]);
    }
    elseif(b[i][j]== 1)
       PrintLCS(b, x, i-1, j);
    else
       PrintLCS(b, x, i, j-1);
}

int main(int argc,char
**argv)
...{
    char x[MAXLEN]=...{"ABCBDAB"};
    char y[MAXLEN]=...{"BDCABA"};
    intb[MAXLEN][MAXLEN];
    intc[MAXLEN][MAXLEN];
    int m, n;
   
    m =strlen(x);
    n =strlen(y);
   
   LCSLength(x, y, m, n, c, b);
   PrintLCS(b, x, m, n);
   
    return 0;
}
#include<stdio.h>

#include<string.h>

#include<stdlib.h>

int fun(char *sa,char *sb)

{

       inti,j,a,b,c,t;

       int  len_a=strlen(sa)+1,len_b=strlen(sb)+1;

       int  * arry=(int*)malloc(len_a*len_b*sizeof(int));//配合上文的说明如何用malloc返回值

   for(i=0;i<len_a;i++)            //强制转化为二重指针

       {

              for(j=0;j<len_b;j++)

              {

                     if(i==0||j==0)arry[i*len_b+j]=0;//这是有一种方法

                     else

                     {

                            a=arry[(i-1)*len_b+j];

                            b=arry[i*len_b+(j-1)];

                            if(sa[i]==sb[j])c=1;

                            else c=0;

                            c=c+arry[(i-1)*len_b+(j-1)];

                            t=a>b?a:b;

                            arry[i*len_b+j]=t>c?t:c;

                     }

              }

       }

       returnarry[(i-1)*len_b+(j-1)];

}

 

int main()

{

       charsa[100];

       charsb[100];

       gets(sa);

       gets(sb);

   printf("%d",fun(sa,sb));

       return0;

}

代码评价:
    这个程序只能输出最长公共子序列的长度,而不能输出序列。思考如何才能输出有多个解的最长公共子序列。

 
 
 
 
 
 
 
 
 
动态规划理解:
我用五个字来总结动态规划,“最优子结构”,有别于通常说的最有子结构。
“子”:体现了动态规划最核心的步骤是找对象的子对象,任何事物都是由很多个“子”构成本身这个总体的。如对象是一个字符串是,它的“子”可以子串,对象是两个字符串时,它的“子”可以是任意两个字串的任意组合。具体还是视题意而定。
“最优”:在建立“子”与“子”之间的递推关系同时,选择最优解。
“结构”:不仅指“子”解是有一定的结构的,而且还指动态规划这一方法就是在一定的结构框架内完成的,还要多加参透。
 
 
 
附录:
    题目标题:翻硬币
 
    小明正在玩一个“翻硬币”的游戏。
 
    桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
 
    比如,可能情形是:**oo***oooo
   
    如果同时翻转左边的两个硬币,则变为:oooo***oooo
 
    现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
    我们约定:把翻动相邻的两个硬币叫做一步操作,那么要求:
  
程序输入:
两行等长的字符串,分别表示初始状态和要达到的目标状态。每行的长度<1000
 
程序输出:
一个整数,表示最小操作步数
 
例如:
用户输入:
**********
o****o****
 
程序应该输出:
5
 
再例如:
用户输入:
*o**o***o***
*o***o**o***
 
程序应该输出:
1
 
题目分析:
咋看之下,这道题也是求俩个字符串之间的距离,但这道题有它的特殊之处在于操作不一样。所以我就从找规律的角度去做了,其实编辑距离这道题也能用找规律的方法去做,但是他考虑的情况有非常多种。而这道题不一样了,通过找规律发现规律很简单。这道题的算法可以不归入五大算法里面。
#include<stdio.h>
#include<string.h>
int main()
{
       charsa[1000],sb[1000];
       intc[1000]={0};
       gets(sa);
       gets(sb);
       intsum=0,i=0,len=strlen(sa),a=0,b=0;
       for(i=0;i<len;i++)if(sa[i]!=sb[i])c[i]=1;
       for(i=0;i<len;i++)
       {
             
              if(c[i]==1)
              {
                     a=i;
                     for(i=i+1;i<len;i++)if(c[i]==1)
                     {
                            b=i;
                            break;
                     }
                     sum+=(b-a);
              }
       }
       printf("%d\n",sum);
       return0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: