1. 程式人生 > >資料結構演算法題/兩個字串的最長公共子序列

資料結構演算法題/兩個字串的最長公共子序列

一,問題描述

給定兩個字串,求解這兩個字串的最長公共子序列(Longest Common Sequence)。比如字串1:BDCABA;字串2:ABCBDAB

則這兩個字串的最長公共子序列長度為4,最長公共子序列是:BCBA

 

二,演算法求解

這是一個動態規劃的題目。對於可用動態規劃求解的問題,一般有兩個特徵:①最優子結構;②重疊子問題

①最優子結構

設 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是兩個序列,將 X 和 Y 的最長公共子序列記為LCS(X,Y)

找出LCS(X,Y)就是一個最優化問題。因為,我們需要找到X 和 Y中最長的

那個公共子序列。而要找X 和 Y的LCS,首先考慮X的最後一個元素和Y的最後一個元素。

1)如果 xn=ym,即X的最後一個元素與Y的最後一個元素相同,這說明該元素一定位於公共子序列中。因此,現在只需要找:LCS(Xn-1,Ym-1)

LCS(Xn-1,Ym-1)就是原問題的一個子問題。為什麼叫子問題?因為它的規模比原問題小。(小一個元素也是小嘛....)

為什麼是最優的子問題?因為我們要找的是Xn-1 和 Ym-1 的最長公共子序列啊。。。最長的!!!換句話說,就是最優的那個。(這裡的最優就是最長的意思)

2)如果xn != ym,這下要麻煩一點,因為它產生了兩個

子問題:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)

因為序列X 和 序列Y 的最後一個元素不相等嘛,那說明最後一個元素不可能是最長公共子序列中的元素嘛。(都不相等了,怎麼公共嘛)。

LCS(Xn-1,Ym)表示:最長公共序列可以在(x1,x2,....x(n-1)) 和 (y1,y2,...yn)中找。

LCS(Xn,Ym-1)表示:最長公共序列可以在(x1,x2,....xn) 和 (y1,y2,...y(n-1))中找。

求解上面兩個子問題,得到的公共子序列誰最長,那誰就是 LCS(X,Y)。用數學表示就是:

LCS=max{LCS(Xn-1,Ym),LCS(Xn,Ym-1)}

由於條件 1)  和  2)  考慮到了所有可能的情況。因此,我們成功地把原問題 轉化 成了 三個規模更小的子問題。

 

②重疊子問題

重疊子問題是啥?就是說原問題 轉化 成子問題後,  子問題中有相同的問題。咦?我怎麼沒有發現上面的三個子問題中有相同的啊????

OK,來看看,原問題是:LCS(X,Y)。子問題有 ❶LCS(Xn-1,Ym-1)    ❷LCS(Xn-1,Ym)    ❸LCS(Xn,Ym-1)

初一看,這三個子問題是不重疊的。可本質上它們是重疊的,因為它們只重疊了一大部分。舉例:

第二個子問題:LCS(Xn-1,Ym) 就包含了:問題❶LCS(Xn-1,Ym-1),為什麼?

因為,當Xn-1 和 Ym 的最後一個元素不相同時,我們又需要將LCS(Xn-1,Ym)進行分解:分解成:LCS(Xn-1,Ym-1) 和 LCS(Xn-2,Ym)

也就是說:在子問題的繼續分解中,有些問題是重疊的。

 

由於像LCS這樣的問題,它具有重疊子問題的性質,因此:用遞迴來求解就太不划算了。因為採用遞迴,它重複地求解了子問題啊。而且注意哦,所有子問題加起來的個數 可是指數級的哦。。。。

這篇文章中就演示了一個遞迴求解重疊子問題的示例。

那麼問題來了,你說用遞迴求解,有指數級個子問題,故時間複雜度是指數級。這指數級個子問題,難道用了動態規劃,就變成多項式時間了??

呵呵噠。。。。

關鍵是採用動態規劃時,並不需要去一 一 計算那些重疊了的子問題。或者說:用了動態規劃之後,有些子問題 是通過 “查表“ 直接得到的,而不是重新又計算一遍得到的。廢話少說:舉個例子吧!比如求Fib數列。關於Fib數列,可參考:

求fib(5),分解成了兩個子問題:fib(4) 和 fib(3),求解fib(4) 和 fib(3)時,又分解了一系列的小問題....

從圖中可以看出:根的左右子樹:fib(4) 和 fib(3)下,是有很多重疊的!!!比如,對於 fib(2),它就一共出現了三次。如果用遞迴來求解,fib(2)就會被計算三次,而用DP(Dynamic Programming)動態規劃,則fib(2)只會計算一次,其他兩次則是通過”查表“直接求得。而且,更關鍵的是:查詢求得該問題的解之後,就不需要再繼續去分解該問題了。而對於遞迴,是不斷地將問題分解,直到分解為 基準問題(fib(1) 或者 fib(0))

說了這麼多,還是要寫下最長公共子序列的遞迴式才完整。借用網友的一張圖吧:)

c[i,j]表示:(x1,x2....xi) 和 (y1,y2...yj) 的最長公共子序列的長度。(是長度哦,就是一個整數嘛)。公式的具體解釋可參考《演算法導論》動態規劃章節

import javax.print.DocFlavor;
import java.util.ArrayList;

/**
 * 動態規劃的方法
 * 採用二維陣列flag來記錄下標i和j的走向。數字"1"表示,斜向下;數字"2"表示,水平向右;數字"3"表示,豎直向下。這樣便於以後的求解最長公共子序列
 */
public class LCS {
    //求解str1 和 str2 的最長公共子序列
    public  int LCS(String str1, String str2, int[][] flag){
        int[][] c = new int[str1.length() + 1][str2.length() + 1];
        for(int row = 0; row <= str1.length(); row++)
            c[row][0] = 0;
        for(int column = 0; column <= str2.length(); column++)
            c[0][column] = 0;

        for(int i = 1; i <= str1.length(); i++)
            for(int j = 1; j <= str2.length(); j++)
            {
                if(str1.charAt(i-1) == str2.charAt(j-1))//兩個list最後一個元素是相同的
                {
                    c[i][j] = c[i - 1][j - 1] + 1;
                    flag[i][j] = 1;  ///斜向下標記
                }
                else if(c[i][j-1] > c[i-1][j])//長的序列在c[i][j-1]裡面,那麼就要c[i][j-1],體現max
                {
                    c[i][j] = c[i][j-1];
                    flag[i][j] = 2;  ///向右標記
                }
                else{
                    c[i][j] = c[i-1][j];//長的序列在c[i-1][j]裡面,那麼就要c[i-1][j],體現max
                    flag[i][j] = 3;  ///向下標記
                }
                //或者直接如下
//                else {
//                    c[i][j] = Math.max( c[i][j-1], c[i-1][j]);
//                }
            }

        return c[str1.length()][str2.length()];
    }


    //下面是遞迴獲取輸出最長子序列,也需要和上面的左右保持一致(即1,2,3)
    public void lcs(int i,int j,String str1,int [][]flag)
    {
        if(i==0||j==0)return;
        if(flag[i][j]==1){
            lcs(i-1,j-1,str1,flag);
            System.out.print(str1.charAt(i));
        }
        else if (flag[i][j]==2)lcs(i,j-1,str1,flag);
        else  lcs(i-1,j,str1,flag);
    }


    public static void main(String[] args) {
        String str1 = "BDCABAE";
        String str2 = "ABCBDABE";
        LCS lcsClass = new LCS();
        int[][] flag = new int[str1.length() + 1][str2.length() + 1]; //標記陣列,用於標識下標的走向,構造出公共子序列
        int result = lcsClass.LCS(str1, str2, flag);
        System.out.println("lcslength:" + String.valueOf(result));
        System.out.println("the lcs is:");
        lcsClass.lcs(str1.length() - 1, str2.length() - 1, str1, flag);
    }
}

轉自https://www.cnblogs.com/hapjin/p/5572483.html