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

前言:

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

 

编辑距离:

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[n][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的差别,可以通过计算他们的最小编辑距离来决定。
      
所谓的编辑距离s1s2变成相同字符串需要下面操作的最小次数。
1.        
把某个字符ch1变成ch2
2.        
删除某个字符
3.        
插入某个字符
例如     s1 = 12433s2=1233;
                    
则可以通过在s2中间插入4得到12433s1一致。
                   
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))
                                         对于23的操作可以等价于:
                                         
_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[n][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 = 0j = 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;

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值