1. 程式人生 > >常考的經典演算法--最長公共子序列(LCS)與最長公共子串(DP)

常考的經典演算法--最長公共子序列(LCS)與最長公共子串(DP)

https://blog.csdn.net/qq_31881469/article/details/77892324

《1》最長公共子序列(LCS)與最長公共子串(DP)

http://blog.csdn.net/u012102306/article/details/53184446

https://segmentfault.com/a/1190000007963594

http://www.cppblog.com/mysileng/archive/2013/05/14/200265.html

1. 問題描述

子串應該比較好理解,至於什麼是子序列,這裡給出一個例子:有兩個母串

  • cnblogs
  • belong

比如序列bo, bg, lg在母串cnblogs與belong中都出現過並且出現順序與母串保持一致,我們將其稱為公共子序列。最長公共子序列(Longest Common Subsequence,LCS),顧名思義,是指在所有的子序列中最長的那一個。子串是要求更嚴格的一種子序列,要求在母串中連續地出現。在上述例子的中,最長公共子序列為blog(cnblogs,belong),最長公共子串為lo(cnblogs, belong)。

2. 求解演算法

對於母串X=<x1,x2,⋯,xm>, Y=<y1,y2,⋯,yn>,求LCS與最長公共子串。
暴力解法
假設 m<n, 對於母串X,我們可以暴力找出2的m次方個子序列,然後依次在母串Y中匹配,演算法的時間複雜度會達到指數級O(n∗2的m次)。顯然,暴力求解不太適用於此類問題。
動態規劃
假設Z=<z1,z2,⋯,zk>是X與Y的LCS, 我們觀察到
如果Xm=Yn,則Zk=Xm=Yn,有Zk−1是Xm−1與Yn−1的LCS;
如果Xm≠Yn,則Zk是Xm與Yn−1的LCS,或者是Xm−1與Yn的LCS。
因此,求解LCS的問題則變成遞迴求解的兩個子問題。但是,上述的遞迴求解的辦法中,重複的子問題多,效率低下。改進的辦法——用空間換時間

,用陣列儲存中間狀態,方便後面的計算。這就是動態規劃(DP)的核心思想了。
DP求解LCS
用二維陣列c[i][j]記錄串x1x2⋯xi與y1y2⋯yj的LCS長度,則可得到狀態轉移方程

 

 

  由最長公共子序列問題的最優子結構性質可知,要找出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的一個最長公共子序列。

 

 

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

 

例如,設所給的兩個序列為X=<A,B,C,B,D,A,B>和Y=<B,D,C,A,B,A>。由演算法LCS_LENGTH和LCS計算出的結果如下圖所示:

 

 

 

程式碼實現

[cpp] view plain copy

 

  1. public static int lcs(String str1, String str2) {  
  2.     int len1 = str1.length();  
  3.     int len2 = str2.length();  
  4.     int c[][] = new int[len1+1][len2+1];  
  5.     for (int i = 0; i <= len1; i++) {  
  6.         for( int j = 0; j <= len2; j++) {  
  7.             if(i == 0 || j == 0) {  
  8.                 c[i][j] = 0;  
  9.             } else if (str1.charAt(i-1) == str2.charAt(j-1)) {  
  10.                 c[i][j] = c[i-1][j-1] + 1;  
  11.             } else {  
  12.                 c[i][j] = max(c[i - 1][j], c[i][j - 1]);  
  13.             }  
  14.         }  
  15.     }  
  16.     return c[len1][len2];  
  17. }  

DP求解最長公共子串

前面提到了子串是一種特殊的子序列,因此同樣可以用DP來解決。定義陣列的儲存含義對於後面推導轉移方程顯得尤為重要,糟糕的陣列定義會導致異常繁雜的轉移方程。考慮到子串的連續性,將二維陣列c[i][j]用來記錄具有這樣特點的子串——結尾同時也為為串x1x2⋯xi與y1y2⋯yj的結尾——的長度。
得到轉移方程:


最長公共子串的長度為 max(c[i,j]), i∈{1,⋯,m},j∈{1,⋯,n}。
程式碼實現

[cpp] view plain copy

 

  1. public static int lcs(String str1, String str2) {  
  2.     int len1 = str1.length();  
  3.     int len2 = str2.length();  
  4.     int result = 0;     //記錄最長公共子串長度  
  5.     int c[][] = new int[len1+1][len2+1];  
  6.     for (int i = 0; i <= len1; i++) {  
  7.         for( int j = 0; j <= len2; j++) {  
  8.             if(i == 0 || j == 0) {  
  9.                 c[i][j] = 0;  
  10.             } else if (str1.charAt(i-1) == str2.charAt(j-1)) {  
  11.                 c[i][j] = c[i-1][j-1] + 1;  
  12.                 result = max(c[i][j], result);  
  13.             } else {  
  14.                 c[i][j] = 0;  
  15.             }  
  16.         }  
  17.     }  
  18.     return result;  
  19. }  

3. 參考資料
[1] cs2035, Longest Common Subsequence.
[2] 一線碼農, 經典演算法題每日演練——第四題 最長公共子序列.

[3] GeeksforGeeks, Dynamic Programming | Set 29 (Longest Common Substring).