最長公共子序列 (LCS) 詳解+例題模板(全)

歡迎訪問https://blog.csdn.net/lxt_Lucia~~

宇宙第一小仙女\(^o^)/~~萌量爆表求帶飛=≡Σ((( つ^o^)つ~ dalao們點個關注唄~~

 

1.摘要:

繼上篇最長上升子序列後,本篇主要講述最長公共子序列 (LCS) 。

 

2.LCS定義:

       最長公共子序列,英文縮寫爲LCS(Longest Common Subsequence)。其定義是,一個序列 S ,如果分別是兩個或多個已知序列的子序列,且是所有符合此條件序列中最長的,則 S 稱爲已知序列的最長公共子序列。

       如果覺得抽象不好理解,那麼咱們還是採用學習LIS的時候的方式。首先,讓我們先來看一下子串子序列還有公共子序列的概念(在上篇LIS中也曾涉及過) ,我們以字符子串和字符子序列爲例,更爲形象,也能順帶着理解字符的子串和子序列:

     (1)字符子串:指的是字符串中連續的n個字符,如abcdefg中,ab,cde,fg等都屬於它的字串。

     (2)字符子序列:指的是字符串中不一定連續但先後順序一致的n個字符,即可以去掉字符串中的部分字符,但不可改變其前後順序。如abcdefg中,acdg,bdf屬於它的子序列,而bac,dbfg則不是,因爲它們與字符串的字符順序不一致。

       (3)  公共子序列:如果序列C既是序列A的子序列,同時也是序列B的子序列,則稱它爲序列A和序列B的公共子序列。如對序列 1,3,5,4,2,6,8,7和序列 1,4,8,6,7,5 來說,序列1,8,7是它們的一個公共子序列。

       那麼現在,我們再通俗的總結一下最長公共子序列(LCS):就是A和B的公共子序列中長度最長的(包含元素最多的)
仍然用序列1,3,5,4,2,6,8,7和序列1,4,8,6,7,5爲例,它們的最長公共子序列有1,4,8,7和1,4,6,7兩種,但最長公共子序列的長度是4。由此可見,最長公共子序列(LCS)也不一定唯一

      請大家用集合的觀點來理解這些概念,子序列、公共子序列以及最長公共子序列都不唯一,所以我們通常特判取一個最長公共子序列,但很顯然,對於固定的兩個數組,雖然最LCS不一定唯一,但LCS的長度是一定的。查找最長公共子序列與查找最長公共子串的問題不同的地方在於:子序列不需要在原序列中佔用連續的位置。最長公共子串(要求連續)和最長公共子序列是不同的。

那麼該如何求出兩個序列的最長公共子序列長度呢?請繼續往下看~

 

3.LCS長度求法:

       你首先能想到的恐怕是暴力枚舉?那我們先來看看:序列A有 2^n 個子序列,序列B有 2^m 個子序列,如果任意兩個子序列一一比較,比較的子序列高達 2^(n+m) 對,這還沒有算具體比較的複雜度。或許你說,只有長度相同的子序列纔會真正進行比較。那麼忽略空序列,我們來看看:對於A長度爲1的子序列有C(n,1)個,長度爲2的子序列有C(n,2)個,……長度爲n的子序列有C(n,n)個。對於B也可以做類似分析,即使只對序列A和序列B長度相同的子序列做比較,那麼總的比較次數高達:C(n,1)*C(m,1)*1 + C(n,2) * C(m,2) * 2+ …+C(n,p) * C(m,p)*p,其中p = min(m, n)。

       嚇着了吧?怎麼辦?我們試試使用動態規劃算法

       我們用Ax表示序列A的連續前x項構成的子序列,即Ax= a1,a2,……ax, By= b1,b2,……by, 我們用LCS(x, y)表示它們的最長公共子序列長度,那原問題等價於求LCS(m,n)。爲了方便我們用L(x, y)表示Ax和By的一個最長公共子序列。讓我們來看看如何求LCS(x, y)。我們令x表示子序列考慮最後一項

(1) Ax = By

         那麼它們L(Ax, By)的最後一項一定是這個元素!

       爲什麼呢?爲了方便,我們令t = Ax = By, 我們用反證法:假設L(x,y)最後一項不是t,則要麼L(x,y)爲空序列(別忘了這個),要麼L(x,y)的最後一項是Aa=Bb ≠ t, 且顯然有a < x, b < y。無論是哪種情況我們都可以把t接到這個L(x,y)後面,從而得到一個更長的公共子序列。矛盾!
       如果我們從序列Ax中刪掉最後一項ax得到Ax-1,從序列By中也刪掉最後一項by得到By-1,(多說一句角標爲0時,認爲子序列是空序列),則我們從L(x,y)也刪掉最後一項t得到的序列是L(x – 1, y - 1)。爲什麼呢?和上面的道理相同,如果得到的序列不是L(x - 1, y - 1),則它一定比L(x - 1, y - 1)短(注意L(,)是個集合!),那麼它後面接上元素t得到的子序列L(x,y)也比L(x - 1, y - 1)接上元素t得到的子序列短,這與L(x, y)是最長公共子序列矛盾。因此L(x, y) = L(x - 1, y - 1) 最後接上元素t,LCS(Ax, By) = LCS(x - 1, y - 1) + 1。

(2)  Ax ≠ By

        仍然設t = L(Ax, By), 或者L(Ax, By)是空序列(這時t是未定義值不等於任何值)。則t  ≠ Ax和t  ≠ By至少有一個成立,因爲t不能同時等於兩個不同的值嘛!

(2.1)如果t  ≠ Ax,則有L(x, y)= L(x - 1, y),因爲根本沒Ax的事嘛。

            LCS(x,y) = LCS(x – 1, y)
(2.2)如果t  ≠ By,l類似L(x, y)= L(x , y - 1)

            LCS(x,y) = LCS(x, y – 1)
       可是,我們事先並不知道t,由定義,我們取最大的一個,因此這種情況下,有LCS(x,y) = max(LCS(x – 1, y) , LCS(x, y – 1))。看看目前我們已經得到了什麼結論:


LCS(x,y) = 
(1) LCS(x - 1,y - 1) + 1      (Ax = By)
(2) max(LCS(x – 1, y) , LCS(x, y – 1))    (Ax ≠ By)

這時一個顯然的遞推式,光有遞推可不行,初值是什麼呢?顯然,一個空序列和任何序列的最長公共子序列都是空序列!所以我們有:

LCS(x,y) = 
(1) LCS(x - 1,y - 1) + 1 如果Ax = By
(2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By
(3) 0 如果x = 0或者y = 0

到此我們求出了計算最長公共子序列長度的遞推公式。我們實際上計算了一個(n + 1)行(m + 1)列的表格(行是0..n,列是0..m),也就這個二維度數組LCS(,)。

大概的僞代碼如下:
輸入序列A, B長度分別爲n,m,計算二維表 LCS(int,int):

for x = 0 to n do
    for y = 0 to m do
        if (x == 0 || y == 0) then 
            LCS(x, y) = 0
        else if (Ax == By) then
            LCS(x, y) =  LCS(x - 1,y - 1) + 1
        else 
            LCS(x, y) = ) max(LCS(x – 1, y) , LCS(x, y – 1))
        endif
    endfor
endfor

注意: 我們這裏使用了循環計算表格裏的元素值,而不是遞歸,如果使用遞歸需要已經記錄計算過的元素,防止子問題被重複計算。

現在問題來了,我們如何得到一個最長公共子序列而僅僅不是簡單的長度呢?其實我們離真正的答案只有一步之遙!

仍然考慮那個遞推式,我們LCS(x,y)的值來源的三種情況:


(1) LCS(x – 1,  y – 1) + 1如果Ax = By
這對應L(x,y) = L(x,- 1 y- 1)末尾接上Ax


(2.1) LCS(x – 1, y)  如果Ax ≠ By且LCS(x – 1, y) ≥LCS(x, y – 1)
這對應L(x,y)= L(x – 1, y)
(2.2) LCS(x, y – 1)  如果Ax ≠ By且LCS(x – 1, y) <LCS(x, y – 1)
這對應L(x,y) = L(x, y – 1)


(3) 0 如果 x =0或者y = 0
這對應L(x,y)=空序列


注意(2.1)和(2.2) ,當LCS(x – 1, y) = LCS(x, y – 1)時,其實走哪個分支都一樣,雖然長度時一樣的,但是可能對應不同的子序列,所以最長公共子序列並不唯一。
神奇吧?又一個類似的遞推公式。可見我們在計算長度LCS(x,y)的時候只要多記錄一些信息,就可以利用這些信息恢復出一個最長公共子序列來。就好比我們在迷宮裏走路,走到每個位置的時候記錄下我們時從哪個方向來的,就可以從終點回到起點一樣。

 


另外,說一下複雜度?時間複雜度時O(n * m),空間也是O(n * m)。

 

4.LCS經典例題模板:

例1:Common Subsequence(求LCS長度)

Description

A subsequence of a given sequence is the given sequence with some elements (possible none) left out. Given a sequence X = <x1, x2, ..., xm> another sequence Z = <z1, z2, ..., zk> is a subsequence of X if there exists a strictly increasing sequence <i1, i2, ..., ik> of indices of X such that for all j = 1,2,...,k, xij = zj. For example, Z = <a, b, f, c> is a subsequence of X = <a, b, c, f, b, c> with index sequence <1, 2, 4, 6>. Given two sequences X and Y the problem is to find the length of the maximum-length common subsequence of X and Y. 
The program input is from a text file. Each data set in the file contains two strings representing the given sequences. The sequences are separated by any number of white spaces. The input data are correct. For each set of data the program prints on the standard output the length of the maximum-length common subsequence from the beginning of a separate line. 

Sample Input

abcfbc abfcab
programming contest 
abcd mnp

Sample Output

4
2
0

思路:

題意是,稱序列 Z = < z1, z2, ..., zk >是序列X = < x1, x2, ..., xm >的子序列當且僅當存在嚴格上升的序列< i1, i2, ..., ik >,使得對j = 1, 2, ... ,k, 有xij = zj。比如Z = < a, b, f, c > 是X = < a, b,c, f, b, c >的子序列。現在給出兩個序列X 和Y,你的任務是找到X 和Y 的最大公共子序列,也就是說要找到一個最長的序列Z,使得Z 既是X 的子序列也是Y 的子序列。

其實就是模板題啦~求LCS長度,當A[i]=A[j]時d(I,j)d(i-1,j-1)+1,否則d(i,j)=max{ d(i-1,j),d(i,j-1) },時間複雜度爲O(n*m),其中n和m分別是序列A和B的長度。

 

代碼:

#include<math.h>
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
#include<queue>

using namespace std;

char a[1001],b[1001];
int dp[1001][1001],len1,len2;

void lcs(int i,int j)
{
    for(i=1;i<=len1;i++)
	{
		for(j=1;j<=len2;j++)
		{
			if(a[i-1]==b[j-1])
				dp[i][j]=dp[i-1][j-1]+1;
			else if(dp[i-1][j]>dp[i][j-1])
				dp[i][j]=dp[i-1][j];
			else
				dp[i][j]=dp[i][j-1];
		}
	}
}

int main()
{
    while(scanf(" %s",a)!=EOF){
    scanf(" %s",b);
    memset(dp,0,sizeof(dp));
    len1=strlen(a);
    len2=strlen(b);
    lcs(len1,len2);
    printf("%d\n",dp[len1][len2]);
    }
    return 0;
}

 

例2:最長公共子序列Lcs(求LCS具體是什麼)

Description

給出兩個字符串A B,求A與B的最長公共子序列(子序列不要求是連續的)。

比如兩個串爲:abcicba 和 abdkscab,則 ab是兩個串的子序列,abc也是,abca也是,其中abca是這兩個字符串最長的子序列。

Input

第1行:字符串A 
第2行:字符串B 
(A,B的長度 <= 1000)

Output

輸出最長的子序列,如果有多個,隨意輸出1個。

Sample Input

abcicba
abdkscab

Sample Output

abca

思路:

       此題的切入點就是動態規劃,通過動歸來確定哪些字符是最長公共子序列中的字符,mat[i][j] 表示第一個序列的前i個字符和第二個序列的前j個字符的公共子序列,動態轉移方程爲:

 dp[i][j] = max(dp[i-1][j], dp[i][j-1],dp[i-1][j-1] + (A[i]==B[j] ? 1 : 0)),表示在這三種狀態中取到最大值,

(1)第一種狀態表示不錄入第一個序列的第i個字符時的最長公共子序列,

(2)第二種狀態表示不錄入第二個序列的第j個字符時的最長公共子序列,

(3)第三種狀態表示第一個序列的前i-1個字符與第二個序列前j-1個字符的公共子序列加上最後一個字符的錄入狀態,如果最後的一個字符相等則錄入狀態爲1,否則爲0。

然後根據動歸的狀態,來判斷我們要求得的序列中的字符有哪些。

 

代碼:

#include<math.h>
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
#include<queue>
#define INF 0x3f3f3f3f

using namespace std;

char a[1001],b[1001];
int dp[1001][1001],len1,len2;

void lcs(int i,int j)
{
    for(i=1; i<=len1; i++)
    {
        for(j=1; j<=len2; j++)
        {
            if(a[i-1]==b[j-1])
                dp[i][j]=dp[i-1][j-1]+1;
            else if(dp[i-1][j]>dp[i][j-1])
                dp[i][j]=dp[i-1][j];
            else
                dp[i][j]=dp[i][j-1];
        }
    }
}

void llcs()
{
    int i,j,z=0;
    char c[1001];
    memset(c,0,sizeof(c));
    i=len1;
    j=len2;
    while(i!=0&&j!=0)
    {
        if(a[i-1]==b[j-1])
        {
            c[z++]=a[--i];
            j--;
        }
        else if(dp[i-1][j]<dp[i][j-1])
            j--;
        else if(dp[i][j-1]<=dp[i-1][j])
            i--;
    }
    for(i=z-1; i>=0; i--)
        printf("%c",c[i]);
    printf("\n");

}

int main()
{
    while(scanf(" %s",a)!=EOF)
    {
        scanf(" %s",b);
        memset(dp,0,sizeof(dp));
        len1=strlen(a);
        len2=strlen(b);
        lcs(len1,len2);
        llcs();
    }
    return 0;
}

 

例3:Advanced Fruits(根據LCS將兩個詞拼接)

Decription

The company "21st Century Fruits" has specialized in creating new sorts of fruits by transferring genes from one fruit into the genome of another one. Most times this method doesn't work, but sometimes, in very rare cases, a new fruit emerges that tastes like a mixture between both of them. 
A big topic of discussion inside the company is "How should the new creations be called?" A mixture between an apple and a pear could be called an apple-pear, of course, but this doesn't sound very interesting. The boss finally decides to use the shortest string that contains both names of the original fruits as sub-strings as the new name. For instance, "applear" contains "apple" and "pear" (APPLEar and apPlEAR), and there is no shorter string that has the same property. 
A combination of a cranberry and a boysenberry would therefore be called a "boysecranberry" or a "craboysenberry", for example. 
Your job is to write a program that computes such a shortest name for a combination of two given fruits. Your algorithm should be efficient, otherwise it is unlikely that it will execute in the alloted time for long fruit names. 

Input

Each line of the input contains two strings that represent the names of the fruits that should be combined. All names have a maximum length of 100 and only consist of alphabetic characters. 
Input is terminated by end of file. 

Output

For each test case, output the shortest name of the resulting fruit on one line. If more than one shortest name is possible, any one is acceptable. 

Sample Input

apple peach
ananas banana
pear peach

Sample Output

appleach
bananas
pearch

思路:

在LCS的基礎之上加上路徑記錄,生成dp數組的時候做上標記,之後按順序輸出結果字符串。注意還要考慮一下沒有公共子序列的情況。

 

代碼:

#include<math.h>
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
#include<queue>

using namespace std;

char a[1001],b[1001],s[10000];
int dp[1001][1001],len1,len2,k=0;

void lcs(int i,int j)
{
    for(i=1; i<=len1; i++)
    {
        for(j=1; j<=len2; j++)
        {
            if(a[i-1]==b[j-1])
                dp[i][j]=dp[i-1][j-1]+1;
            else if(dp[i-1][j]>dp[i][j-1])
                dp[i][j]=dp[i-1][j];
            else
                dp[i][j]=dp[i][j-1];
        }
    }
}

void llcs()
{
    int i,j,z=0,k=0;
    i=len1;
    j=len2;
    while(dp[i][j])
    {
        if(a[i-1]==b[j-1])
        {
            s[k++]=a[--i];
            j--;
        }
        else if(dp[i][j-1]<dp[i-1][j])
        {
            s[k++]=a[--i];
        }
        else if(dp[i][j-1]>=dp[i-1][j])
        {
            s[k++]=b[--j];
        }
    }
    while(i!=0)
        s[k++]=a[--i];
    while(j!=0)
        s[k++]=b[--j];
    for(z=k-1;z>=0;z--)
        printf("%c",s[z]);
    printf("\n");
}

int main()
{
    while(scanf(" %s",a)!=EOF)
    {
        scanf(" %s",b);
        memset(dp,0,sizeof(dp));
        len1=strlen(a);
        len2=strlen(b);
        lcs(len1,len2);
        llcs();
    }
    return 0;
}

 

 

5.相關知識:( 建議放在一起比較區分 )

1)最長上升子序列  ( LIS )  戳這裏

2)最長迴文子串 and 最長迴文子序列  (LPS)  戳這裏 

 

 

模板都在這兒呢~ 快去AC幾發叭~

 

 

宇宙第一小仙女\(^o^)/~~萌量爆表求帶飛=≡Σ((( つ^o^)つ~ dalao們點個關注唄~~

 

 

參考資料:https://blog.csdn.net/lz161530245/article/details/76943991

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