編輯距離與最長公共子序列總結

前言:

其實編輯距離和最長公共子序列是對同一個問題的描述,都能夠顯示出兩個字符串之間的“相似度”,即它們的雷同程度。而子序列與字串的區別在於字串是連續的,子序列可以不連續,只要下標以此遞增就行。

 

編輯距離:

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;

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章