1. 程式人生 > >動態規劃演算法解最長公共子序列LCS問題

動態規劃演算法解最長公共子序列LCS問題

原文地址:http://blog.csdn.net/rrrfff/article/details/7523437

動態規劃演算法解LCS問題

作者 July 二零一零年十二月三十一日

本文參考:微軟面試100題系列V0.1版第19、56題、演算法導論、維基百科。

第一部分、什麼是動態規劃演算法

ok,咱們先來了解下什麼是動態規劃演算法。

動態規劃一般也只能應用於有最優子結構的問題。最優子結構的意思是區域性最優解能決定全域性最優解(對有些問題這個要求並不能完全滿足,故有時需要引入一定的近似)。簡單地說,問題能夠分解成子問題來解決。

動態規劃演算法分以下4個步驟:

  1. 描述最優解的結構
  2. 遞迴定義最優解的值
  3. 按自底向上的方式計算最優解的值 //此3步構成動態規劃解的基礎。
  4. 由計算出的結果構造一個最優解。 //此步如果只要求計算最優解的值時,可省略。

好,接下來,咱們討論適合採用動態規劃方法的最優化問題的倆個要素:最優子結構性質,和子問題重疊性質。

  • 最優子結構

如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。意思就是,總問題包含很多個子問題,而這些子問題的解也是最優的。

  • 重疊子問題

子問題重疊性質是指在用遞迴演算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重複計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果儲存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地檢視一下結果,從而獲得較高的效率。

第二部分、動態規劃演算法解LCS問題

下面,咱們運用此動態規劃演算法解此LCS問題。有一點必須宣告的是,LCS問題即最長公共子序列問題,它要求所求得的字元在所給的字串中是連續的(例如:輸入兩個字串BDCABA和ABCBDAB,字串BCBA和BDAB都是是它們的最長公共子序列,則輸出它們的長度4,並列印任意一個子序列)。

ok,咱們馬上進入面試題第56題的求解,即運用經典的動態規劃演算法:

2.0、LCS問題描述

56.最長公共子序列。
題目:如果字串一的所有字元按其在字串中的順序出現在另外一個字串二中,
則字串一稱之為字串二的子串。

注意,並不要求子串(字串一)的字元必須連續出現在字串二中。
請編寫一個函式,輸入兩個字串,求它們的最長公共子串,並打印出最長公共子串。
例如:輸入兩個字串BDCABA和ABCBDAB,字串BCBA和BDAB都是是它們的最長公共子序列,則輸出它們的長度4,並列印任意一個子序列。

分析:求最長公共子序列(Longest Common Subsequence, LCS)是一道非常經典的動態規劃題,因此一些重視演算法的公司像MicroStrategy都把它當作面試題。

事實上,最長公共子序列問題也有最優子結構性質。

記:

Xi=﹤x1,⋯,xi﹥即X序列的前i個字元 (1≤i≤m)(字首)

Yj=﹤y1,⋯,yj﹥即Y序列的前j個字元 (1≤j≤n)(字首)

假定Z=﹤z1,⋯,zk﹥∈LCS(X , Y)。

  • xm=yn(最後一個字元相同),則不難用反證法證明:該字元必是X與Y的任一最長公共子序列Z(設長度為k)的最後一個字元,即有zk = xm = yn 且顯然有Zk-1∈LCS(Xm-1 , Yn-1)即Z的字首Zk-1是Xm-1與Yn-1的最長公共子序列。此時,問題化歸成求Xm-1與Yn-1的LCS(LCS(X , Y)的長度等於LCS(Xm-1 , Yn-1)的長度加1)。

  • xm≠yn,則亦不難用反證法證明:要麼Z∈LCS(Xm-1, Y),要麼Z∈LCS(X , Yn-1)。由於zk≠xm與zk≠yn其中至少有一個必成立,若zk≠xm則有Z∈LCS(Xm-1 , Y),類似的,若zk≠yn 則有Z∈LCS(X , Yn-1)。此時,問題化歸成求Xm-1與Y的LCS及X與Yn-1的LCS。LCS(X , Y)的長度為:max{LCS(Xm-1 , Y)的長度, LCS(X , Yn-1)的長度}。

由於上述當xm≠yn的情況中,求LCS(Xm-1 , Y)的長度與LCS(X , Yn-1)的長度,這兩個問題不是相互獨立的:兩者都需要求LCS(Xm-1,Yn-1)的長度。另外兩個序列的LCS中包含了兩個序列的字首的LCS,故問題具有最優子結構性質考慮用動態規劃法。

也就是說,解決這個LCS問題,你要求三個方面的東西:1、LCS(Xm-1,Yn-1)+1;2、LCS(Xm-1,Y),LCS(X,Yn-1);3、max{LCS(Xm-1,Y),LCS(X,Yn-1)}

2.1、最長公共子序列的結構

最長公共子序列的結構有如下表示:

設序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的一個最長公共子序列Z=<z1, z2, …, 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的最長公共子序列。

其中Xm-1=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>,Zk-1=<z1, z2, …, zk-1>。

2.2、子問題的遞迴結構

由最長公共子序列問題的最優子結構性質可知,要找出X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最長公共子序列,可按以下方式遞迴地進行:當xm=yn時,找出Xm-1和Yn-1的最長公共子序列,然後在其尾部加上xm(=yn)即可得X和Y的一個最長公共子序列。當xm≠yn時,必須解兩個子問題,即找出Xm-1和Y的一個最長公共子序列及X和Yn-1的一個最長公共子序列。這兩個公共子序列中較長者即為X和Y的一個最長公共子序列。

由此遞迴結構容易看到最長公共子序列問題具有子問題重疊性質。例如,在計算X和Y的最長公共子序列時,可能要計算出X和Yn-1及Xm-1和Y的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算Xm-1和Yn-1的最長公共子序列。

與矩陣連乘積最優計算次序問題類似,我們來建立子問題的最優值的遞迴關係。用c[i,j]記錄序列Xi和Yj的最長公共子序列的長度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。當i=0或j=0時,空序列是Xi和Yj的最長公共子序列,故c[i,j]=0。其他情況下,由定理可建立遞迴關係如下:

2.3、計算最優值

直接利用上節節末的遞迴式,我們將很容易就能寫出一個計算c[i,j]的遞迴演算法,但其計算時間是隨輸入長度指數增長的。由於在所考慮的子問題空間中,總共只有θ(m*n)個不同的子問題,因此,用動態規劃演算法自底向上地計算最優值能提高演算法的效率。

計算最長公共子序列長度的動態規劃演算法LCS_LENGTH(X,Y)以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作為輸入。輸出兩個陣列c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]儲存Xi與Yj的最長公共子序列的長度,b[i,j]記錄指示c[i,j]的值是由哪一個子問題的解達到的,這在構造最長公共子序列時要用到。最後,X和Y的最長公共子序列的長度記錄於c[m,n]中。

[cpp] view plain copy  print?
  1. Procedure LCS_LENGTH(X,Y);  
  2. begin  
  3.   m:=length[X];  
  4.   n:=length[Y];  
  5.   for i:=1 to m do c[i,0]:=0;  
  6.   for j:=1 to n do c[0,j]:=0;  
  7.   for i:=1 to m do
  8.     for j:=1 to n do
  9.       if x[i]=y[j] then  
  10.         begin  
  11.           c[i,j]:=c[i-1,j-1]+1;  
  12.           b[i,j]:="↖";  
  13.         end  
  14.       elseif c[i-1,j]≥c[i,j-1] then  
  15.         begin  
  16.           c[i,j]:=c[i-1,j];  
  17.           b[i,j]:="↑";  
  18.         end  
  19.       else
  20.         begin  
  21.           c[i,j]:=c[i,j-1];  
  22.           b[i,j]:="←"
  23.         end;  
  24.   return(c,b);  
  25. end;  

由演算法LCS_LENGTH計算得到的陣列b可用於快速構造序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最長公共子序列。首先從b[m,n]開始,沿著其中的箭頭所指的方向在陣列b中搜索。

  • 當b[i,j]中遇到"↖"時(意味著xi=yi是LCS的一個元素),表示Xi與Yj的最長公共子序列是由Xi-1與Yj-1的最長公共子序列在尾部加上xi得到的子序列;
  • 當b[i,j]中遇到"↑"時,表示Xi與Yj的最長公共子序列和Xi-1與Yj的最長公共子序列相同;
  • 當b[i,j]中遇到"←"時,表示Xi與Yj的最長公共子序列和Xi與Yj-1的最長公共子序列相同。

這種方法是按照反序來找LCS的每一個元素的。由於每個陣列單元的計算耗費Ο(1)時間,演算法LCS_LENGTH耗時Ο(mn)。

2.4、構造最長公共子序列

下面的演算法LCS(b,X,i,j)實現根據b的內容打印出Xi與Yj的最長公共子序列。通過演算法的呼叫LCS(b,X,length[X],length[Y]),便可打印出序列X和Y的最長公共子序列。

[cpp] view plain copy  print?
  1. Procedure LCS(b,X,i,j);  
  2. begin  
  3.   if i=0 or j=0 then return;  
  4.   if b[i,j]="↖" then  
  5.     begin  
  6.       LCS(b,X,i-1,j-1);  
  7.       print(x[i]); {列印x[i]}  
  8.     end  
  9.   elseif b[i,j]="↑" then LCS(b,X,i-1,j)   
  10.                       else LCS(b,X,i,j-1);  
  11. end;   

在演算法LCS中,每一次的遞迴呼叫使i或j減1,因此演算法的計算時間為O(m+n)。

我來說明下此圖(參考演算法導論)。在序列X={A,B,C,B,D,A,B}和 Y={B,D,C,A,B,A}上,由LCS_LENGTH計算出的表c和b。第i行和第j列中的方塊包含了c[i,j]的值以及指向b[i,j]的箭頭。在c[7,6]的項4,表的右下角為X和Y的一個LCS的長度。對於i,j>0,項c[i,j]僅依賴於是否有xi=yi,及項c[i-1,j]和c[i,j-1]的值,這幾個項都在c[i,j]之前計算。為了重構一個LCS的元素,從右下角開始跟蹤b[i,j]的箭頭即可,這條路徑標示為陰影,這條路徑上的每一個“↖”對應於一個使xi=yi為一個LCS的成員的項(高亮標示)。

所以根據上述圖所示的結果,程式將最終輸出:“B C B A”,或“B D A B”。

可能還是有讀者對上面的圖看的不是很清楚,下面,我再通過對最大子序列,最長公共子串與最長公共子序列的比較來闡述相關問題@Orisun:

  • 最大子序列:最大子序列是要找出由陣列成的一維陣列中和最大的連續子序列。比如{5,-3,4,2}的最大子序列就是{5,-3,4,2},它的和是8,達到最大;而{5,-6,4,2}的最大子序列是{4,2},它的和是6。你已經看出來了,找最大子序列的方法很簡單,只要前i項的和還沒有小於0那麼子序列就一直向後擴充套件,否則丟棄之前的子序列開始新的子序列,同時我們要記下各個子序列的和,最後找到和最大的子序列。更多請參看:程式設計師程式設計藝術第七章、求連續子陣列的最大和
  • 最長公共子串:找兩個字串的最長公共子串,這個子串要求在原字串中是連續的。其實這又是一個序貫決策問題,可以用動態規劃來求解。我們採用一個二維矩陣來記錄中間的結果。這個二維矩陣怎麼構造呢?直接舉個例子吧:"bab"和"caba"(當然我們現在一眼就可以看出來最長公共子串是"ba"或"ab")

       b  a  b

    c  0  0  0

    a  0  1  0

    b  1  0  1

    a  0  1  0

    我們看矩陣的斜對角線最長的那個就能找出最長公共子串。

    不過在二維矩陣上找最長的由1組成的斜對角線也是件麻煩費時的事,下面改進:當要在矩陣是填1時讓它等於其左上角元素加1。

       b  a  b

    c  0  0  0

    a  0  1  0

    b  1  0  2

    a  0  2  0

    這樣矩陣中的最大元素就是最長公共子串的長度。

    在構造這個二維矩陣的過程中由於得出矩陣的某一行後其上一行就沒用了,所以實際上在程式中可以用一維陣列來代替這個矩陣。

  • 最長公共子序列LCS問題:最長公共子序列與最長公共子串的區別在於最長公共子序列不要求在原字串中是連續的,比如ADE和ABCDE的最長公共子序列是ADE。

    我們用動態規劃的方法來思考這個問題如是求解。首先要找到狀態轉移方程:

    等號約定,C1是S1的最右側字元,C2是S2的最右側字元,S1‘是從S1中去除C1的部分,S2'是從S2中去除C2的部分。

    LCS(S1,S2)等於:

(1)LCS(S1,S2’)

(2)LCS(S1’,S2)

(3)如果C1不等於C2:LCS(S1’,S2’);如果C1等於C2:LCS(S1',S2')+C1;

邊界終止條件:如果S1和S2都是空串,則結果也是空串。

下面我們同樣要構建一個矩陣來儲存動態規劃過程中子問題的解。這個矩陣中的每個數字代表了該行和該列之前的LCS的長度。與上面剛剛分析出的狀態轉移議程相對應,矩陣中每個格子裡的數字應該這麼填,它等於以下3項的最大值:

(1)上面一個格子裡的數字

(2)左邊一個格子裡的數字

(3)左上角那個格子裡的數字(如果C1不等於C2);左上角那個格子裡的數字+1(如果C1等於C2)

舉個例子:

     G  C  T  A

   0  0  0  0  0

G  0  1  1  1  1

B  0  1  1  1  1

T  0  1  1  2  2

A 0  1  1  2  3

填寫最後一個數字時,它應該是下面三個的最大者:

(1)上邊的數字2

(2)左邊的數字2

(3)左上角的數字2+1=3,因為此時C1==C2

所以最終結果是3。

在填寫過程中我們還是記錄下當前單元格的數字來自於哪個單元格,以方便最後我們回溯找出最長公共子串。有時候左上、左、上三者中有多個同時達到最大,那麼任取其中之一,但是在整個過程中你必須遵循固定的優先標準。在我的程式碼中優先級別是左上>左>上。

下圖給出了回溯法找出LCS的過程:

2.5、演算法的改進

對於一個具體問題,按照一般的演算法設計策略設計出的演算法,往往在演算法的時間和空間需求上還可以改進。這種改進,通常是利用具體問題的一些特殊性。

例如,在演算法LCS_LENGTH和LCS中,可進一步將陣列b省去。事實上,陣列元素c[i,j]的值僅由c[i-1,j-1],c[i-1,j]和c[i,j-1]三個值之一確定,而陣列元素b[i,j]也只是用來指示c[i,j]究竟由哪個值確定。因此,在演算法LCS中,我們可以不借助於陣列b而藉助於陣列c本身臨時判斷c[i,j]的值是由c[i-1,j-1],c[i-1,j]和c[i,j-1]中哪一個數值元素所確定,代價是Ο(1)時間。既然b對於演算法LCS不是必要的,那麼演算法LCS_LENGTH便不必儲存它。這一來,可節省θ(mn)的空間,而LCS_LENGTH和LCS所需要的時間分別仍然是Ο(mn)和Ο(m+n)。不過,由於陣列c仍需要Ο(mn)的空間,因此這裡所作的改進,只是在空間複雜性的常數因子上的改進。

另外,如果只需要計算最長公共子序列的長度,則演算法的空間需求還可大大減少。事實上,在計算c[i,j]時,只用到陣列c的第i行和第i-1行。因此,只要用2行的陣列空間就可以計算出最長公共子序列的長度。更進一步的分析還可將空間需求減至min(m, n)。

第三部分、最長公共子序列問題程式碼

ok,最後給出此面試第56題的程式碼,參考程式碼如下,請君自看:

[cpp] view plain copy  print?
  1. // LCS.cpp : 定義控制檯應用程式的入口點。
  2. //
  3. //[email protected]
  4. //[email protected] July
  5. #include "stdafx.h"
  6. #include "string.h"
  7. #include <iostream>
  8. usingnamespace std;  
  9. // directions of LCS generation
  10. enum decreaseDir {kInit = 0, kLeft, kUp, kLeftUp};  
  11. void LCS_Print(int

    相關推薦

    動態規劃演算法公共序列LCS問題

    原文地址:http://blog.csdn.net/rrrfff/article/details/7523437 動態規劃演算法解LCS問題 作者 July 二零一零年十二月三十一日 本文參考:

    動態規劃演算法--公共序列問題

    如今最好,沒有來日方長! 一、簡述動態規劃演算法 1.動態規劃演算法簡介 (1)背景 動態規劃(英語:Dynamic programming,簡稱:DP)是一種演算法設計技術,是運籌學的一個分支,是求解決策過程(Decision pr

    Python 動態規劃演算法求解公共序列

    前言:在網上看到一道360的秋招真題,題目如下: 仔細讀題後發現這是一道求解最長公共子序列的問題,最好使用動態規劃演算法。 題目大意: 小B坐火車,從起點到終點的車站序列已知,期間他睡了兩覺,到終點的時候還在睡,也就是說中間他醒了兩次,這兩次清醒的時間,有兩個車站子序列,

    【經典動態規劃問題】公共序列LCS

    目錄 題目 題目分析 狀態 邊界值討論 其他情況討論 程式碼實現 從題目出發分析如何用動態規劃求解最長公共子序列問題 題目 給定兩個字串A和B,返回兩個字串的最長公共子序列的長度。例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”1234

    演算法導論-----公共序列LCS動態規劃

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

    演算法學習——動態規劃 例題:公共序列問題(java)

    題目: 給定兩個字串str1和str2,返回兩個字串的最長公共子序列.例如,str1="1A2C3D4B56",str2="B1D23CA45B6A","123456"或者"12C4B6' 動態規劃思想: 先用一個比,左邊加一個字元右面加一個字元依次比較dp[i][j] dp[i][j]意思

    動態規劃:求公共序列公共

    最長公共子序列(LCS): 這同樣是一道經典題目,定義就不說了。 為了方便說明,我們用Xi代表{x1,x2,‥xi},用Yj代表{y1,y2,‥yj}。那麼,求長度分別為m,n的兩個序列X,Y的LCS就相當於求Xm與Yn的LCS。我們將其分割為區域性問題進行分析。 首先,求Xm與Yn的LCS要考慮一下兩

    動態規劃專題之公共序列

    動態規劃系列專題講義 專題三:最長公共子序列 /* Name: 動態規劃專題之最長公共子序列 Author: 巧若拙 Description: 1808_公共子序列 描述:我們稱序列Z = < z1, z2, ..., zk >是序列X = < x1, x2,

    動態規劃入門之公共序列LCS

    LCS是動態規劃在字串問題中應用的典型。問題描述:給定2個序列,求這兩個序列的最長公共子序列,不要求子序列連續。例如{2,4,3,1,2,1}和{1,2,3,2,4,1,2}的結果是{2,3,2,1}或者{2,4,1,2}。 思路:如果不用動態規劃去做,而用暴力法,則必須找

    動態規劃】NYOJ公共序列

    最長公共子序列 時間限制:3000 ms  |  記憶體限制:65535 KB 難度:3 描述咱們就不拐彎抹角了,如題,需要你做的就是寫一個程式,得出最長公共子序列。 tip:最長公共子序

    演算法分析—公共序列(LCS)

    子序列:給定一個序列X={x1,x2,…,xm},另一個序列Z={z1,z2,…,zk},即存在一個嚴格遞增的X的下標序列{i1,i2,…,ik},對於所有的j=1,2,3…,k,滿足xij=zj,我

    演算法課堂實驗報告(四)——python動態規劃公共序列LCS問題)

    python實現動態規劃 一、開發環境 開發工具:jupyter notebook 並使用vscode,cmd命令列工具協助程式設計測試演算法,並使用codeblocks輔助編寫C++程式 程式語言:python3.6 二、實驗內容 1.最長公共子序列問題。分別求x=

    公共序列LCS迴文序列動態規劃演算法

    c[i][j] 用來表示 x[i] 和y[j] 的LCS 長度 對c[i][j], 若i = 0或j = 0,則c[i][j] = 0, 兩個原序列有一個為空,則沒有LCS; 若i,j > 0 且x[i] = y[j],則c[i][j] = c[i-1][j-1] +

    動態規劃-公共序列LCS

    return str2 pat for 思路 規劃 得來 表示 || 0 問題 給定兩個字符串,求最長公共子序列LCS。 也就是說兩個字符串中都有的部分,或者理解為,兩個字符串同時都刪除字符串中的某些字符,使得最終的兩個字符串,相等,且是最長的。 1 分析 假設兩個str1

    動態規劃 公共序列(LCS) 過程圖解

    1.基本概念       首先需要科普一下,最長公共子序列(longest common sequence)和最長公共子串(longest common substring)不是一回事兒。什麼是子序列呢?即一個給定的序列的子序列,就是將給定序列中零個或多個元素去掉之後得到的

    【經典問題】二維動態規劃問題:求公共序列LCS

    原博地址:http://blog.csdn.net/yysdsyl/article/details/4226630                 http://blog.csdn.net/ljyljyok/article/details/77905681    證明:   

    演算法學習——動態規劃 例題:遞增序列(java)

    給定陣列arr,返回arr的最長遞增子序列長度。比如arr=[2,1,5,3,6,4,8,9,7]最長遞增子序列為, [1,3,4,8,9] ,所以返回這個子序列的長度為5,給定陣列arr, 返回arr的最長所以返回這個子序列的長度。比如arr=[2,1,5,3,6,4,8,9,7] 最長遞增子序列

    動態規劃】求公共串,迴文

    題目 : 給定兩個字串,求出它們之間連續的最長的相同子字串的長度。 eg : fbaabe,ebaabf,連續最長子串長度為4。 注意:求最長迴文子串也可以用求最長公共子串來求,只需將字串反轉作為另外一個字串,迴文部分反轉之後不變,然後求LCS(Longes

    【51nod】---1006 公共序列Lcs動態規劃&&字串LCS

    題目連結這裡呀 1006 最長公共子序列Lcs 基準時間限制:1 秒 空間限制:131072 KB 分值: 0 難度:基礎題 收藏 關注 給出兩個字串A B,求A與B的最長公共子序列(子序列不要求是連續的)。 比如兩個串為: abcicba abd

    動態規劃----公共序列(LCS)問題

    生成 現在 public add tostring -s 序列 highlight arr 題目:   求解兩個字符串的最長公共子序列。如 AB34C 和 A1BC2 則最長公共子序列為 ABC。   思路分析:可以用dfs深搜,這裏使用到了前面沒有見到過的雙重循環