1. 程式人生 > >編輯距離與最長公共子序列總結

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

前言:

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

編輯距離:

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 100void 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);elsePrintLCS(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;

}