一,問題描述html
給定兩個字符串,求解這兩個字符串的最長公共子序列(Longest Common Sequence)。好比字符串1:BDCABA;字符串2:ABCBDAB算法
則這兩個字符串的最長公共子序列長度爲4,最長公共子序列是:BCBA數組
二,算法求解post
這是一個動態規劃的題目。對於可用動態規劃求解的問題,通常有兩個特徵:①最優子結構;②重疊子問題優化
①最優子結構spa
設 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是兩個序列,將 X 和 Y 的最長公共子序列記爲LCS(X,Y).net
找出LCS(X,Y)就是一個最優化問題。由於,咱們須要找到X 和 Y中最長的那個公共子序列。而要找X 和 Y的LCS,首先考慮X的最後一個元素和Y的最後一個元素。code
1)若是 xn=ym,即X的最後一個元素與Y的最後一個元素相同,這說明該元素必定位於公共子序列中。所以,如今只須要找:LCS(Xn-1,Ym-1)htm
LCS(Xn-1,Ym-1)就是原問題的一個子問題。爲何叫子問題?由於它的規模比原問題小。(小一個元素也是小嘛....)blog
爲何是最優的子問題?由於咱們要找的是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) 的最長公共子序列的長度。(是長度哦,就是一個整數嘛)。公式的具體解釋可參考《算法導論》動態規劃章節
三,LCS JAVA實現
1 public class LCSequence { 2 3 //求解str1 和 str2 的最長公共子序列 4 public static int LCS(String str1, String str2){ 5 int[][] c = new int[str1.length() + 1][str2.length() + 1]; 6 for(int row = 0; row <= str1.length(); row++) 7 c[row][0] = 0; 8 for(int column = 0; column <= str2.length(); column++) 9 c[0][column] = 0; 10 11 for(int i = 1; i <= str1.length(); i++) 12 for(int j = 1; j <= str2.length(); j++) 13 { 14 if(str1.charAt(i-1) == str2.charAt(j-1)) 15 c[i][j] = c[i-1][j-1] + 1; 16 else if(c[i][j-1] > c[i-1][j]) 17 c[i][j] = c[i][j-1]; 18 else 19 c[i][j] = c[i-1][j]; 20 } 21 return c[str1.length()][str2.length()]; 22 } 23 24 //test 25 public static void main(String[] args) { 26 String str1 = "BDCABA"; 27 String str2 = "ABCBDAB"; 28 int result = LCS(str1, str2); 29 System.out.println(result); 30 } 31 }
感受整個代碼就是直接根據上面的那個遞歸表達式寫的。
①第5行定義一個數組來保存最長公共子序列的長度
②第6行至第9行是初始化。爲何初始化成0? 由於: c[0,j]表示啥?表示字符串1的長度是0,字符串2的長度是j,這兩個字符串的最長公共子序列的長度是?固然是0 嘍。。。由於,字符串1 根本就沒有嘛。
③第11行至第20行,就是遞歸表達式的程序表示。第16行至第19行,就是: c[i,j] = max{c[i][j-1], c[i-1][j]}
④第21行返回最終結果。爲何是返回 c[str1.length()][str2.length()]???看看 c[i][j]表示什麼意思,你就知道了。
四,參考資料
https://www.zhihu.com/question/23995189
http://www.cnblogs.com/huangxincheng/archive/2012/11/11/2764625.html
http://www.hawstein.com/posts/dp-knapsack.html