【演算法】動態規劃法——最長公共子序列(LCS)
前言
這篇是自己寫的第一篇關於演算法方面的部落格,寫他是因為自己今天開啟筆記,剛好看到了它,就這麼簡單。
這篇部落格主要想講講動態規劃法,然後以LCS問題為例展開來說一下怎麼利用動態規劃法求解它,下面是自己的一些理解和總結,有不對的地方還請大家指正。
動態規劃法
動態規劃法(dynamic programming)通常用於求解最優化問題(optimization problem),它適用於那些子問題相互重疊的情況,即子問題不獨立,不同的子問題具有公共的子子問題(就是子問題的子問題)。這顯然與分治法是不同的,分治法將問題劃分為不重疊的子問題,然後分別求解這些子問題,最後將這些問題合併得到最終的解。
對於具有公共子問題的情況,分治法會做很多不必要的工作,它會多次求解同一子子問題。動態規劃法卻不一樣,對每個子子問題它只會求解一次,將其儲存在一個表格中,避免了不必要的重複計算。
如之前所說,動態規劃法用於求解最優化問題,這就意味著可能這個問題,有很多解,但是呢,不一定都是最優解。利用動態規劃法求出來的是這個問題的一個最優解(an optimal solution),記住這裡求解的只是最優解(the optimal solution)中的一個,因為最優解可能有多個。
設計一個問題的動態規劃演算法主要有一下的幾步
(1) 找出最優解的性質,刻畫其結構特徵;
(2) 遞迴的定義最優解的值;
(3) 以自底向上的方式計算出最優值;
(4) 根據計算最優解時得到的資訊,構造一個最優解。
如果你只需要一個最優解的值,而不是這個結本身,就不需要第(4)步。如果你需要得到這個解本身,也就是說你需要執行第(4)步,這往往需要我們在第(3)步中記錄一些額外的資訊,以方便第(4)步的求解。
下面讓我們來看看LCS問題如何利用動態規劃法求解。
最長公共子序列的動態規劃法實現
最長公共子序列(longest-common-subsequence, LCS)
(1)子序列:一個序列X = x1
例如:對序列 1,3,5,4,2,6,8,7來說,序列3,4,8,7 是它的一個子序列。對於一個長度為n的序列,它一共有2^n 個子序列,有(2^n – 1)個非空子序列。在這裡需要提醒大家,子序列不是子集,它和原始序列的元素順序是相關的。
(2)公共子序列:如果序列Z既是序列X的子序列,同時也是序列Y的子序列,則稱它為序列X和序列Y的公共子序列。空序列是任何兩個序列的公共子序列。
(3)最長公共子序列:X和Y的公共子序列中長度最長的(包含元素最多的)叫做X和Y的最長公共子序列。
這個問題如果用窮舉法時間,最終求出最長公共子序列時,時間複雜度是Ο(2mn),是指數級別的複雜度,對於長序列是不適用的。因此我們使用動態規劃法來求解。
刻畫最長公共子序列問題的最優子結構
設X=x1x2…xm和Y=y1y2…yn是兩個序列,Z=z1z2…zk是這兩個序列的一個最長公共子序列。
1. 如果xm=yn,那麼zk=xm=yn,且Zk-1是Xm-1,Yn-1的一個最長公共子序列;
2. 如果xm≠yn,那麼zk≠xm,意味著Z是Xm-1,Y的一個最長公共子序列;
3. 如果xm≠yn,那麼zk≠yn,意味著Z是X,Yn-1的一個最長公共子序列。
從上面三種情況可以看出,兩個序列的LCS包含兩個序列的字首的LCS。因此,LCS問題具有最優子結構特徵。
遞迴的定義最優值
從最優子結構可以看出,如果xm=yn,那麼我們應該求解Xm-1,Yn-1的一個LCS,並且將xm=yn加入到這個LCS的末尾,這樣得到的一個新的LCS就是所求。
如果xm≠yn,我們需要求解兩個子問題,分別求Xm-1,Y的一個LCS和X,Yn-1的一個LCS。兩個LCS中較長者就是X和Y的一個LCS。
可以看出LCS問題具有重疊子問題性質。為了求X和Y的一個LCS,我們需要分別求出Xm-1,Y的一個LCS和X,Yn-1的一個LCS,這幾個字問題又包含了求出Xm-1,Yn-1的一個LCS的子子問題。(有點繞了。。。暈沒暈。。。。)
根據上面的分析,我們可以得出下面的公式;
計算最優解的值
根據上面的,我們很容易就可以寫出遞迴計算LCS問題的程式,通過這個程式我們可以求出各個子問題的LCS的值,此外,為了求解最優解本身,我們好需要一個表b,b[i,j]記錄使C[i,j]取值的最優子結構。
C++程式碼如下;
int **Lcs_length(string X,string Y,int **B)
{
int x_len = X.length();
int y_len = Y.length();
int **C = new int *[x_len+1];
for (int i = 0; i <= x_len; i++)
{
C[i] = new int[y_len + 1]; //定義一個存放最優解的值的表;
}
for (int i = 1; i <= x_len; i++)
{
C[i][0] = 0;
B[i][0] = -2; //-2表示沒有方向
}
for (int j = 0; j <= y_len; j++)
{
C[0][j] = 0;
B[0][j] = -2;
}
for (int i = 1; i <= x_len; i++)
{
for (int j = 1; j <= y_len; j++)
{
if (X[i-1]==Y[j-1])
{
C[i][j] = C[i - 1][j - 1] + 1;
B[i][j] = 0; //0表示斜向左上
}
else
{
if (C[i-1][j]>=C[i][j-1])
{
C[i][j] = C[i - 1][j];
B[i][j] = -1; //-1表示豎直向上;
}
else
{
C[i][j] = C[i][j - 1];
B[i][j] = 1; //1表示橫向左
}
}
}
}
return C;
}
將C與b分別輸出的記過如下圖
將兩個表格畫成一個表格的結果如下;
構造最長公共子序列
從表格中可以看出,用表b中的資訊可以構建出X和Y的一個LCS。從b[m,n]開始,沿著箭頭的方向追蹤,當箭頭是斜上的時候,表示Xi=Yj;是LCS中的一個元素。C++程式碼如下;
void OutPutLCS(int **B, string X,int str1_len,int str2_len)
{
if (str1_len == 0 || str2_len == 0)
{
return;
}
if (B[str1_len][str2_len] == 0) //箭頭左斜
{
OutPutLCS(B, X, str1_len - 1, str2_len - 1);
printf("%c", X[str1_len - 1]);
}
else if (B[str1_len][str2_len] == -1)
{
OutPutLCS(B, X, str1_len - 1, str2_len);
}
else
{
OutPutLCS(B, X, str1_len, str2_len-1);
}
}
最終輸出的結果是BCBA,但是如之前所說,這只是所有最優解中的一個,明顯BDAB也是一個最優解。
演算法改進
從方法的實現中可以看出計算LCS的過程中時間複雜度是Ο(mn),空間複雜度也是Ο(mn)。在構造最長公共子序列的過程中時間複雜度為Ο(m+n);但是我們可以不適用表b,我們可以直接利用表C的資訊構造LCS;
因為C[i,j]的值只依賴於三項:C[i-1,j],C[i,j-1],C[i-1,j-1]。這樣就為記憶體節約了一個Ο(mn)空間。但是計算LCS的輔助空間並未減少,因為表C的空間複雜度依然是Ο(mn)。這部分工作利用C++很容易就能實現,我就不貼出來了。
再談動態規劃法
通過上面的過程想必大家對於動態規劃法已經有了一定的理解。對他的計算效果也肯定是非常認同的。
動態規劃法是一個非常有效的演算法設計技術,它主要用於具有以下兩種特徵的問題。
(1)最優子結構。如果一個問題的最優解中包含了其子問題的最優解,就說該問題具有最優子結構。當一個問題具有最優子結構時,我們就可以考慮使用動態規劃法去實現它。
(2)重疊子問題。重疊子問題是指用來解原問題的遞迴演算法會反覆求解同樣子問題,當一個遞迴演算法不斷地呼叫同一個問題時,就說明該問題包含了重疊子問題。此時如果用分治法求解,會反覆求解同樣的問題,效率低下。
完整程式碼
// 動態規劃法解決最長子序列.cpp : 定義控制檯應用程式的入口點。
//
#include "stdafx.h"
#include <string>
#include <iostream>
#ifndef MAX
#define MAX(X,Y) ((X>=Y)? X:Y)
#endif
using namespace std;
int **Lcs_length(string X,string Y,int **B)
{
int x_len = X.length();
int y_len = Y.length();
int **C = new int *[x_len+1];
for (int i = 0; i <= x_len; i++)
{
C[i] = new int[y_len + 1]; //定義一個存放最優解的值的表;
}
for (int i = 1; i <= x_len; i++)
{
C[i][0] = 0;
B[i][0] = -2; //-2表示沒有方向
}
for (int j = 0; j <= y_len; j++)
{
C[0][j] = 0;
B[0][j] = -2;
}
for (int i = 1; i <= x_len; i++)
{
for (int j = 1; j <= y_len; j++)
{
if (X[i-1]==Y[j-1])
{
C[i][j] = C[i - 1][j - 1] + 1;
B[i][j] = 0; //0表示斜向左上
}
else
{
if (C[i-1][j]>=C[i][j-1])
{
C[i][j] = C[i - 1][j];
B[i][j] = -1; //-1表示豎直向上;
}
else
{
C[i][j] = C[i][j - 1];
B[i][j] = 1; //1表示橫向左
}
}
}
}
return C;
}
void OutPutLCS(int **B, string X,int str1_len,int str2_len)
{
if (str1_len == 0 || str2_len == 0)
{
return;
}
if (B[str1_len][str2_len] == 0) //箭頭左斜
{
OutPutLCS(B, X, str1_len - 1, str2_len - 1);
printf("%c", X[str1_len - 1]);
}
else if (B[str1_len][str2_len] == -1)
{
OutPutLCS(B, X, str1_len - 1, str2_len);
}
else
{
OutPutLCS(B, X, str1_len, str2_len-1);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
string X = "ABCBDAB";
string Y = "BDCABA";
int x_len = X.length();
int y_len = Y.length();
int **C;
int **B = new int *[x_len + 1];
for (int i = 0; i <= x_len; i++)
{
B[i] = new int[y_len + 1];
}
C = Lcs_length(X, Y, B);
for (int i = 0; i <= x_len; i++)
{
for (int j = 0; j <= y_len; j++)
{
cout << C[i][j]<<" ";
}
cout << endl;
}
cout << endl;
for (int i = 0; i <= x_len; i++)
{
for (int j = 0; j <= y_len; j++)
{
cout << B[i][j] << " ";
}
cout << endl;
}
OutPutLCS(B, X, x_len, y_len);
system("pause");
return 0;
已完。。