使用動態規劃解決最長公共子序列問題
一、定義:
給定兩個序列X和Y,如果Z既是X的子序列也是Y的子序列,那麼我們稱Z是X和Y的公共子序列。例如:X={a,b,c,e,d,g,f},Y={b,e,f,g},那麼Z={b}、Z={b,e}、Z={b,e,f}都是X和Y的公共子序列,其中Z={b,e,f}是X和Y的最長公共子序列。求解X和Y的最長公共子序列就是LCS問題。最長公共子序列不唯一,但是其長度是唯一的。
二、定理:
令X={x1,x2,...,xm}和Y={y1,y2,....yn}為兩個序列,Z={z1,z2,...zk}為這兩個序列的最長公共子序列,則:
①如果xm=yn,那麼zk = xm = yn,且Z(k-1)是X(m-1)和Y(n-1)的一個最長公共子序列;
②如果xm != yn,zk != xm,那麼Z(k-1)是X(m-1)和Yn的一個最長公共子序列;
③如果xm != yn,zk != yn,那麼Z(k-1)是Xm和Y(n-1)的一個最長公共子序列;
這樣我們在求解X和Y的LCS時,可以分解為求解一個或兩個這樣的子問題。當xm = yn,我們需要求解X(m-1)和Y(n-1)的LCS;當xm != yn時,我們需要求解X(m-1)和Yn的一個LCS,以及求解Xm和Y(n-1)的一個LCS,兩個較長者既為X和Y的LCS。
三、求解步驟:
如果用暴力搜尋法求解LCS問題,就要窮舉X的所有子序列,對於每個子序列都要檢查它是否也是Y的子序列,從而找到最長子序列。X的每個子序列對應下表集合{1,2,3,....,m},所以X有2^m個子序列,因此暴力搜尋法的執行時間為指數階,對較長的序列是不適用的,但是根據以上定理,LCS問題具有最優子結構性質。
我們定義一個二維陣列c[i][j]用來記錄Xi和Yj的LCS的長度。如果i=0,或者j=0,既一個序列長度為0,那麼LCS的長度為0。根據最優子結構性質,可得如下公式
上述公式適用於字串下標為0時儲存的是字串的長度,然而我們常見的字串是'\0'為結尾的,下標為0的內容儲存的並不是字串的長度,因此我們把公式稍作修改以適應解決我們常見的字串
根據以上公式我們,可以用動態規劃方法自頂向上地計算c[i][j]的值。
以下為計算c[i][j]的值,並給出測試程式:
#include <iostream> #define MAX_LEN 100 using namespace std; /*給定兩個字串X和Y,求出Xi和Yj的最長公共子序列的長度,記錄在c[i][j]陣列中*/ void get_lcslength(const char X[],int Xlen, const char Y[],int Ylen, int c[][MAX_LEN]); int main() { char X[] = "abaddc"; char Y[] = "badcaef"; int m = strlen(X); int n = strlen(Y); int c[MAX_LEN][MAX_LEN]; get_lcslength(X, m, Y, n, c); for (int i = 0; i < m;i++) { for (int j = 0; j < n;j++) { cout << c[i][j] <<" "; } cout << endl; } return 0; } <pre name="code" class="cpp">void get_lcslength(const char X[],int Xlen, const char Y[],int Ylen, int c[][MAX_LEN]) { int i = 0, j = 0; /*初始化第0行,如果X[0]=Y[i],則說明LCS的長度為1*/ while (i < Ylen && X[0] != Y[i]) { c[0][i] = 0; i++; } for (; i < Ylen; i++) c[0][i] = 1; /*同理,初始化第0列*/ while (j < Xlen && Y[0] != X[j]) { c[j][0] = 0; j++; } for (; j < Xlen; j++) c[j][0] = 1; for (i = 1; i < Xlen;i++) { for (j = 1; j < Ylen;j++) { if (X[i] == Y[j]) c[i][j] = c[i - 1][j - 1] + 1; else if (c[i - 1][j] >= c[i][j - 1]) c[i][j] = c[i - 1][j]; else c[i][j] = c[i][j - 1]; } } }
程式的執行結果如下:
我們一張表格來解釋上面的程式:
第i行和第j列記錄了c[i][j]的值,對於第0行第0列X[0] != Y[0],故c[0][0]=0,對於第0行第1列,X[0]=Y[1],因此c[0][1]=1,那麼第0行從第1列開始及以後的列都為1;同理可求出第0列的所有c[i][0]的值。對於i>0,j>0的,標項c[i][j]的值取決於X[i]是否等於Y[j]及c[i-1][j-1]、c[i-1][j]、c[i][j-1]的值,這些值都會在計算c[i][j]的值之前計算出來。c[5][6]記錄了X和Y的LCS的長度。
當我們求出二維陣列c的值之後,我們可以根據這個二維陣列求解LCS,通過上述分析我們可以瞭解到:c[i][j]的值只取決於c[i-1][j-1]、c[i-1][j]、c[i][j-1]這三項,給出c[i][j]的值我們可以在O(1)時間內判斷出計算c[i][j]的值使用了這三項中的哪一項,我們配合下面的圖說明一下演算法的實現原理:
圖中的箭頭表示當計算c[i][j]時所使用的上一個元素是誰,首先c[i][j]與c[i][j-1]作比較,如果相等則表明計算c[i][j]時使用的是c[i][j-1],此時令j=j-1;否則,c[i][j]與c[i-1][j]作比較,如果相等則表明計算c[i][j]時使用的是c[i-1][j],此時令i=i-1;否則表明計算c[i][j]時使用的是c[i-1][j-1],那麼這時X[i] == Y[j],將這個相等的字元記錄到str陣列中,並令j=j-1,i=i-1。進行迴圈比較直到i、j有一個出現0時為止。程式碼如下:
#include <iostream>
#define MAX_LEN 100
using namespace std;
/*給定兩個字串X和Y,求出Xi和Yj的最長公共子序列的長度,記錄在c[i][j]陣列中*/
void get_lcslength(const char X[],int Xlen, const char Y[],int Ylen, int c[][MAX_LEN]);
/*給定兩個字串X和Y,將最長公共子序列儲存在str陣列中*/
char *lcs(const char X[], const char Y[], char str[]);
int main()
{
char X[] = "abaddc";
char Y[] = "badcaef";
char str[MAX_LEN];
lcs(X, Y, str);
cout << "最長公共子序列為:" << str << " 長度為:" << strlen(str)<<endl;
lcs("acbecda", "abcdaef", str);
cout << "最長公共子序列為:" << str << " 長度為:" << strlen(str) << endl;
lcs("gcbecda", "hbcdaef", str);
cout << "最長公共子序列為:" << str << " 長度為:" << strlen(str) << endl;
lcs("ghijklmna", "hbcdaef", str);
cout << "最長公共子序列為:" << str << " 長度為:" << strlen(str) << endl;
lcs("gijklmn", "hbcdaef", str);
cout << "最長公共子序列為:" << str << " 長度為:" << strlen(str) << endl;
return 0;
}
void get_lcslength(const char X[],int Xlen, const char Y[],int Ylen, int c[][MAX_LEN])
{
int i = 0, j = 0;
/*初始化第0行,如果X[0]=Y[i],則說明LCS的長度為1*/
while (i < Ylen && X[0] != Y[i])
{
c[0][i] = 0;
i++;
}
for (; i < Ylen; i++)
c[0][i] = 1;
/*同理,初始化第0列*/
while (j < Xlen && Y[0] != X[j])
{
c[j][0] = 0;
j++;
}
for (; j < Xlen; j++)
c[j][0] = 1;
for (i = 1; i < Xlen;i++)
{
for (j = 1; j < Ylen;j++)
{
if (X[i] == Y[j])
c[i][j] = c[i - 1][j - 1] + 1;
else if (c[i - 1][j] >= c[i][j - 1])
c[i][j] = c[i - 1][j];
else
c[i][j] = c[i][j - 1];
}
}
}
char *lcs(const char X[], const char Y[], char str[])
{
int Xlen = strlen(X);
int Ylen = strlen(Y);
int c[MAX_LEN][MAX_LEN];
get_lcslength(X, Xlen, Y, Ylen, c);
int i = Xlen - 1;
int j = Ylen - 1;
str[c[i][j]] = '\0';//此時的c[i][j]記錄著最長公共子序列的長度,讓str以'\0'結尾
while (i > 0 && j > 0)
{
if (c[i][j] == c[i][j - 1])
j = j - 1;
else if (c[i][j] == c[i - 1][j])
i = i - 1;
else
{
str[c[i][j] - 1] = X[i];
i = i - 1;
j = j - 1;
}
}
/*判斷第0行或者第0列的值,如果為1則表明X[i] == Y[j]*/
if (c[i][j] == 1)
{
if (i == 0)
{
str[0] = X[0];
}
if (j == 0)
{
str[0] = Y[0];
}
}
return str;
}
程式的執行結果如下所示: