最長公共子序列LCS(動態規劃)—詳解

一.基本概念

1、 子序列(subsequence): 一個特定序列的子序列就是將給定序列中零個或多個元素去掉後得到的結果(不改變元素間相對次序)。例如序列<A,B,C,B,D,A,B><A,B,C,B,D,A,B>的子序列有:<A,B><A,B>、<B,C,A><B,C,A>、<A,B,C,D,A><A,B,C,D,A>等。

2、公共子序列(common subsequence): 給定序列X和Y,序列Z是X的子序列,也是Y的子序列,則Z是X和Y的公共子序列。例如X=<A,B,C,B,D,A,B>X=<A,B,C,B,D,A,B>,Y=<B,D,C,A,B,A>Y=<B,D,C,A,B,A>,那麼序列Z=<B,C,A>Z=<B,C,A>爲X和Y的公共子序列,其長度爲3。但ZZ不是XX和YY的最長公共子序列,而序列<B,C,B,A><B,C,B,A>和<B,D,A,B><B,D,A,B>也均爲XX和YY的最長公共子序列,長度爲4,而XX和YY不存在長度大於等於5的公共子序列。

3、最長公共子序列問題(LCS:longest-common-subsequence problem):In the longest-common-subsequence problem, we are given two sequences X=<x1,x2,…,xm>X=<x1,x2,…,xm> and Y=<y1,y2,…,yn>Y=<y1,y2,…,yn> and wish to find a (not “the”) maximum-length common subsequence of XX and YY . This section shows how to efficiently solve the LCS problem using dynamic programming.

其次科普一下,最長公共子序列(longest common sequence)和最長公共子串(longest common substring)不是一回事兒。什麼是子序列呢?即一個給定的序列的子序列,就是將給定序列中零個或多個元素去掉之後得到的結果。什麼是子串呢?給定串中任意個連續的字符組成的子序列稱爲該串的子串。給一個圖再解釋一下:
在這裏插入圖片描述在這裏插入圖片描述如上圖,給定的字符序列: {a,b,c,d,e,f,g,h},它的子序列示例: {a,c,e,f} 即元素b,d,g,h被去掉後,保持原有的元素序列所得到的結果就是子序列。同理,{a,h},{c,d,e}等都是它的子序列。它的字串示例:{c,d,e,f} 即連續元素c,d,e,f組成的串是給定序列的字串。同理,{a,b,c,d},{g,h}等都是它的字串。

這個問題說明白後,最長公共子序列(以下都簡稱LCS)就很好理解了。

給定序列s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2},s1和s2的相同子序列,且該子序列的長度最長,即是LCS。s1和s2的其中一個最長公共子序列是 {3,4,6,7,8}

二.最長公共子序列解決方案

方案1:蠻力搜索策略
蠻力搜索策略的步驟如下:

  • 枚舉序列XX裏的每一個子序列xixi;
  • 檢查子序列xixi是否也是YY序列裏的子序列;
  • 在每一步記錄當前找到的子序列裏面的最長的子序列。

蠻力策略也叫做暴力窮舉法,是所有算法中最直觀的方法,但效率往往也是最差的。在第1步枚舉XX中所有的子序列有2m2m個,每個子序列在YY中檢查時間複雜度爲O(n)O(n)。因此蠻力策略的最壞時間複雜度爲O(n2m)O(n2m),這是指數級算法,對較長的序列求LCS是不適用的。

2.動態規劃

動態規劃算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規劃算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。如果我們能夠保存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重複計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。

特徵分析

解決LCS問題,需要把原問題分解成若干個子問題,所以需要刻畫LCS的特徵。

   設A=“a0,a1,…,am”,B=“b0,b1,…,bn”,且Z=“z0,z1,…,zk”爲它們的最長公共子序列。不難證明有以下性質:
   如果am=bn,則zk=am=bn,且“z0,z1,…,z(k-1)”是“a0,a1,…,a(m-1)”和“b0,b1,…,b(n-1)”的一個最長公共子序列;
   如果am!=bn,則若zk!=am,蘊涵“z0,z1,…,zk”是“a0,a1,…,a(m-1)”和“b0,b1,…,bn”的一個最長公共子序列;
   如果am!=bn,則若zk!=bn,蘊涵“z0,z1,…,zk”是“a0,a1,…,am”和“b0,b1,…,b(n-1)”的一個最長公共子序列。

有些同學,一看性質就容易暈菜,所以我給出一個圖來讓這些同學理解一下:
在這裏插入圖片描述以我在第1小節舉的例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),並結合上圖來說:

假如S1的最後一個元素 與 S2的最後一個元素相等,那麼S1和S2的LCS就等於 {S1減去最後一個元素} 與 {S2減去最後一個元素} 的 LCS 再加上 S1和S2相等的最後一個元素。

假如S1的最後一個元素 與 S2的最後一個元素不等(本例子就是屬於這種情況),那麼S1和S2的LCS就等於 : {S1減去最後一個元素} 與 S2 的LCS, {S2減去最後一個元素} 與 S1 的LCS 中的最大的那個序列。

4.遞歸公式

第3節說了LCS的特徵,我們可以發現,假設我需要求 a1 … am 和 b1 … b(n-1)的LCS 和 a1 … a(m-1) 和 b1 … bn的LCS,一定會遞歸地並且重複地把如a1… a(m-1) 與 b1 … b(n-1) 的 LCS 計算幾次。所以我們需要一個數據結構來記錄中間結果,避免重複計算。

假設我們用c[i,j]表示Xi 和 Yj 的LCS的長度(直接保存最長公共子序列的中間結果不現實,需要先借助LCS的長度)。其中X = {x1 … xm},Y ={y1…yn},Xi = {x1 … xi},Yj={y1… yj}。可得遞歸公式如下:
C[i,j]={0 若 i=0 或 j=0C[i1,j1]+1 若 i,j>0,xi=yjmax{C[i,j1],C[i1,j]} 若 i,j>0,xiyjC[i, j]=\left\{\begin{array}{ll}0 & \text { 若 } i=0 \text { 或 } j=0 \\ C[i-1, j-1]+1 & \text { 若 } i, j>0, x_{i}=y_{j} \\ \max \{C[i, j-1], C[i-1, j]\} & \text { 若 } i, j>0, x_{i} \neq y_{j}\end{array}\right.

5.計算LCS的長度

這裏還是以s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2}爲例。我們借用《算法導論》中的推導圖:
在這裏插入圖片描述
圖中的空白格子需要填上相應的數字(這個數字就是c[i,j]的定義,記錄的LCS的長度值)。填的規則依據遞歸公式,簡單來說:如果橫豎(i,j)對應的兩個元素相等,該格子的值 = c[i-1,j-1] + 1。如果不等,取c[i-1,j] 和 c[i,j-1]的最大值。首先初始化該表:

然後,一行一行地從上往下填:

在這裏插入圖片描述
S1的元素3 與 S2的元素3 相等,所以 c[2,1] = c[1,0] + 1。繼續填充:
在這裏插入圖片描述
S1的元素3 與 S2的元素5 不等,c[2,2] =max(c[1,2],c[2,1]),圖中c[1,2] 和 c[2,1] 背景色爲淺黃色。

繼續填充:

在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述
中間幾行填寫規則不變,直接跳到最後一行:

在這裏插入圖片描述
至此,該表填完。根據性質,c[8,9] = S1 和 S2 的 LCS的長度,即爲5。

構造LCS

本文S1和S2的最長LCS並不是只有1個,本文並不是着重講輸出兩個序列的所有LCS,只是介紹如何通過上表,輸出其中一個LCS。

我們根據遞歸公式構建了上表,我們將從最後一個元素c[8][9]倒推出S1和S2的LCS。

c[8][9] = 5,且S1[8] != S2[9],所以倒推回去,c[8][9]的值來源於c[8][8]的值(因爲c[8][8] > c[7][9])。

c[8][8] = 5, 且S1[8] = S2[8], 所以倒推回去,c[8][8]的值來源於 c[7][7]。

以此類推,如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 這種存在分支的情況,這裏請都選擇一個方向(之後遇到這樣的情況,也選擇相同的方向)。

第一種結果爲:
在這裏插入圖片描述
這就是倒推回去的路徑,棕色方格爲相等元素,即LCS = {3,4,6,7,8},這是其中一個結果。

如果如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 這種存在分支的情況,選擇另一個方向,會得到另一個結果。

在這裏插入圖片描述即LCS ={3,5,7,7,8}。

關於時間複雜度

構建c[i][j]表需要Θ(mn),輸出1個LCS的序列需要Θ(m+n)。

代碼實現

#include <iostream>

using namespace std;

/*
* 這裏可以不定義長度,輸入的字符串用string存儲,然後利用string.c_str()來對字符串進行數組轉化。 我這裏爲了方便沒有這樣做。
*/
#ifndef MAX_LENGTH
#define MAX_LENGTH 15 //定義字符串最大長度
#endif

int MaxNum(int firstNum, int secondNum){
    return firstNum > secondNum ? firstNum : secondNum;
}

//定義數組結構體
struct matrix{
    int num;
    int direct;
};

typedef matrix Matrix;

int LCS(char *strA, char *strB, int lengthA, int lengthB, Matrix *resultMatrix[]){
    if (lengthA == 0 || lengthB == 0) {
        return 0;
    }
    for (int i = 0; i < lengthA; i++) {
        for (int j = 0; j < lengthB; j++) {
            resultMatrix[i][j].num = 0; //設置所有默認的最長爲0
            resultMatrix[i][j].direct = 1; //所有默認方向變成上 0斜上,1上,-1左
        }
    }

    for (int i = 0; i < lengthA; i++) {
        for (int j = 0; j < lengthB; j++) {
            if (strA[i] == strB[j]) {
                resultMatrix[i+1][j+1].num = resultMatrix[i][j].num + 1;
                resultMatrix[i+1][j+1].direct = 0;
            }else{
                resultMatrix[i+1][j+1].num = MaxNum(resultMatrix[i+1][j].num, resultMatrix[i][j+1].num);
                resultMatrix[i+1][j+1].direct = resultMatrix[i+1][j].num > resultMatrix[i][j+1].num ? 1 : -1;
            }
        }
    }
    return resultMatrix[lengthA][lengthB].num;
}

int main(int argc, const char * argv[]) {
    char *strA = (char*)malloc(sizeof(char) * MAX_LENGTH);
    char *strB = (char*)malloc(sizeof(char) * MAX_LENGTH);
    scanf("%s",strA);
    scanf("%s",strB);
    int lengthA = (int)strlen(strA);
    int lengthB = (int)strlen(strB);
    Matrix *resultMatrix[lengthA+1];
    for (int i = 0; i <= lengthA; i++) {
        resultMatrix[i] = (Matrix*)malloc(sizeof(struct matrix)* (lengthB+1));
    }

    int max = LCS(strA, strB, lengthA, lengthB, resultMatrix);
    printf("%d\n",max);
    std::cout << "Hello, World!\n";
    return 0;
}

參考自:
Running07

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章