1. 程式人生 > >演算法學習之二——用DP和備忘錄演算法求解最長公共子序列問題

演算法學習之二——用DP和備忘錄演算法求解最長公共子序列問題

問題定義:

最長公共子序列:給定兩個序列X={x1,x2,……xn},Y={y1,y2……,ym},如果X的子序列存在一個嚴格遞增的下標序列{,,……},使得對於所有的j=1,2……,k,有=,則稱產生的陣列為對應的公共子序列。

如果公共子序列的長度最大,我們就稱之為最長公共子序列,並求出LCS的長度(最優值)和對應的子序列(最優解)。

我們將和所對應的長度儲存在數組裡,並記錄為c[i][j]。

我們可以證明問題的求解具有:

1.最優子結構性質。 問題的最優解包括子問題的最優解,不同子問題的最優解疊加在一起就是總問題的最優解

2.和重疊子問題:子問題有多個且具有重複性,如果兩個序列不存在任何相同的元素,則c[1][1]=c[m][n]。

基於以上兩點性質,可以用動態規劃的演算法來進行求解。順便一提,在ACM題目中有很多這樣的題,任何求序列問題(如MaxSum)和樹的問題(如二叉搜尋樹),幾乎不用證明就可以去求解。

直接給出公式:

二 問題求解兼程式碼:

在已經給出公式的前提下,以下為所進行的的四種方法:

1.對窮舉法: 我的思路是定義一個向量組Z,儲存所有X元素存在的情況,|Z|=(2^n)*n,且Zn∈{0,1}。之後按照if(Zn,n∈1→n)的判斷方式,每次都可以生成X的一個子序列,只要依次判斷Y中是否存在公共子序列,儲存並更新對應的長度,就能求出LCS的長度。但所耗費的時間複雜度太大。並且程式往往會卡住,執行不出來。

2.對直接遞迴法:在公式已經給定的情況下,最簡單的思考方式就是進行直接遞迴。

我的虛擬碼如下:

<span style="font-size:14px;">Int Lcs_length(int i,int j){
If(i==0 || j==0) return 0;  //遞迴的邊界條件
Else if(x[i-1]==y[j-1]) return Lcs_length(i-1,j-1)+1;  //每一次都要進行新的遞迴
Else return max(LCS_length(i,j-1),LCS_length(i-1,j)); }</span>

直接遞迴法的缺點是顯而易見的,每一次進行計算的時候都要重複計算。比如我拿(6,6)來進行舉例,(6,6)第一次遞迴後的情況是(6,5)和(5,6),兩者分別進行遞迴後又是(5,5)、(6,4)和(5,5)、(4,6),也就是說(5,5)被重複計算了兩次。如果存在大量不相等元素的話,就會因此產生大量的冗餘,影響執行效率。

如果我們能夠將每一次的結果(必要的)都儲存下來,就可以節省大量的時間,因此匯出了第三種方法。

 3.備忘錄法:備忘錄實際上也是從上往下進行遞迴求解,只是每次都將求解的值記錄下來,避免了大量的重複計算。

我的虛擬碼如下:

在直接遞迴法的基礎上,只在程式碼的最後面加上return c[i][j],作為每一次遞迴呼叫的返回值。

其他情況下將return改為 c[i][j],記錄在數組裡即可。

具體實現如圖: 其中p[100]本來是要求具體的序列的,但實現起來發現幾乎不可能。

    memset(c,-1,sizeof(c));
    strcpy(x,"ABCBDAB");
    strcpy(y,"BDCABA");
    int m=strlen(x),n=strlen(y);
    cout<<x<<endl;cout<<y<<endl;
 cout<<"the length:"<<Memorized_LCS(m,n)<<endl;

count-=1;
while(count--) cout<<p[count]<<endl;
        return 0;
}

int Memorized_LCS(int i,int j)
{   memset(c,-1,sizeof(c));
    strcpy(x,"ABCBDAB");
    strcpy(y,"BDCABA");
    int m=strlen(x),n=strlen(y);
    cout<<x<<endl;cout<<y<<endl;
    if (c[i][j]>-1) return c[i][j]; //已經被計算過,就不用再次計算。
    if(i==0 || j==0) c[i][j]=0; //邊界條件
    else if(x[i-1]==y[j-1])  {c[i][j]=Memorized_LCS(i-1,j-1)+1;p[count++]=x[i-1];}//因為不知道這次被呼叫的最終
    //最終是否會被納入總的結果,因此可以認為是無法求出序列的
    else /*if(Memorized_LCS(i-1,j)> Memorized_LCS(i,j-1)) c[i][j]=Memorized_LCS(i-1,j);
        else c[i][j]=Memorized_LCS(i,j-1);*/
             c[i][j]=max(Memorized_LCS(i,j-1),Memorized_LCS(i-1,j)); //max是自帶的函式
    return c[i][j];

}




4.動態規劃(DP)法:

動態規劃法的思想同樣來源於直接遞迴法,只不過提前就將每次求解需要用到的c[i-1][j-1]都提前求好。記錄下來。

具體的求解過程如下所示:設兩個子串的長度分別為n,m。則定義矩陣c[n+1][m+1],變化量從0到n,儲存從子串x[1..n]和y[1..m]的子序列的長度。顯然c[i][0]和c[0][j]都為0。

接下來按照c[1][1..m],c[2][1..m],……c[n][1..m]的順序自左向右來填每行的表。並且每一次遞迴時都記錄下填表的方式,用1、2、3分別表示來自於c[i-1][j-1]、c[i-1][j]、c[i][j-1],這樣在求出最優值的同時也能逆推出最優解。

程式碼如下:

#include<iostream>
#include<cstring>
using namespace std;
#define max 1000

int c[max][max];
char x[100],y[100],z[100];
int display[100][100];

int fillform(int n,int m){
    //並沒有進行遞迴呼叫
    memset(c,0,sizeof(c));
    memset(z,0,sizeof(z));
    memset(display,0,sizeof(display));
    int i,j,k;


    //先填行和縱列,之後再每行每列的填表
    for(i=1;i<=n;i++)
        for(j=1;j<=m;j++){
        if(x[i-1]==y[j-1]) {c[i][j]=c[i-1][j-1]+1;display[i][j]=1;}
    else if(c[i][j-1]>c[i-1][j]) {c[i][j]=c[i][j-1];display[i][j]=2;}
       else {c[i][j]=c[i-1][j];display[i][j]=3;} } //這時已經算出了所有的情況來
   cout<<"test:"<<c[3][3]<<c[n][m-1]<<endl;
     return c[n][m];
}

void show()
{

    int n=strlen(x),m=strlen(y);
    int i,j,k;

    int count=0; //對應不同的作用
    while(n>0 && m>0) //不能是>=0
    {
        if(display[n][m]==1){
            z[count++]=x[n-1];n--;m--;}
            else if(display[n][m]==2) m--; //向上移動一層
             else if(display[n][m]==3) n--; //向左移動一層
    }

    while(--count) cout<<z[count];cout<<z[0];
}

int main(void)
{
   int count=5000;
   strcpy(x,"ABCBDAB");
    strcpy(y,"BDCABA");

   //cout<<x[1]<<y[1]<<endl;
    //cout<<strlen(x)<<" "<<strlen(y)<<endl;
    cout<<x<<endl;cout<<y<<endl;
         while(count--){
    cout<<"the length:"<<fillform(strlen(x),strlen(y))<<endl;}
    cout<<"the sequence:";show();

    return 0;
}