最長公共子序列,英文縮寫為LCS(Longest Common Subsequence)。其定義是,一個序列 S ,如果分別是兩個或多個已知序列的子序列,且是所有符合此條件序列中最長的,則 S 稱為已知序列的最長公共子序列。而最長公共子串(要求連續)和最長公共子序列是不同的.
最長公共子序列是一個十分實用的問題,它可以描述兩段文字之間的"相似度",即它們的雷同程度,從而能夠用來辨別抄襲。對一段文字進行修改之后,計算改動前后文字的最長公共子序列,將除此子序列外的部分提取出來,這種方法判斷修改的部分,往往十分准確。
動態規划法
經常會遇到復雜問題不能簡單地分解成幾個子問題,而會分解出一系列的子問題。簡單地采用把大問題分解成子問題,並綜合子問題的解導出大問題的解的方法,問題求解耗時會按問題規模呈冪級數增加。為了節約重復求相同子問題的時間,引入一個數組,不管它們是否對最終解有用,把所有子問題的解存於該數組中,這就是動態規划法所采用的基本方法。
算法
動態規划的一個計算兩個序列的最長公共子序列的方法如下:
以兩個序列 X、Y 為例子:
設有二維數組f[i,j] 表示 X 的 i 位和 Y 的 j 位之前的最長公共子序列的長度,則有:
f[1][1] = same(1,1);
f[i,j] = max{f[i-1][j -1] + same(i,j),f[i-1,j],f[i,j-1]}
其中,same(a,b)當 X 的第 a 位與 Y 的第 b 位相同時為"1",否則為"0"。
此時,二維數組中最大的數便是 X 和 Y 的最長公共子序列的長度,依據該數組回溯,便可找出最長公共子序列。
該算法的空間、時間復雜度均為O(n^2),經過優化后,空間復雜度可為O(n)。
【問題】 求兩字符序列的最長公共字符子序列
問題描述:字符序列的子序列是指從給定字符序列中隨意地(不一定連續)去掉若干個字符(可能一個也不去掉)后所形成的字符序列。令給定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一個嚴格遞增下標序列<i0,i1,…,ik-1>,使得對所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一個子序列。
考慮最長公共子序列問題如何分解成子問題,設A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,並Z=“z0,z1,…,zk-1”為它們的最長公共子序列。不難證明有以下性質:
(1) 如果am-1=bn-1,則zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一個最長公共子序列;
(2) 如果am-1!=bn-1,則若zk-1!=am-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列;
(3) 如果am-1!=bn-1,則若zk-1!=bn-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列。
這樣,在找A和B的公共子序列時,如有am-1=bn-1,則進一步解決一個子問題,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一個最長公共子序列;如果am-1!=bn-1,則要解決兩個子問題,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列,再取兩者中較長者作為A和B的最長公共子序列。
求解:
引進一個二維數組c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜索的方向。
我們是自底向上進行遞推計算,那么在計算c[i,j]之前,c[i-1][j-1],c[i-1][j]與c[i][j-1]均已計算出來。此時我們根據X[i] = Y[j]還是X[i] != Y[j],就可以計算出c[i][j]。

回溯輸出最長公共子序列過程:

我來說明下此圖(參考算法導論)。在序列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<B,C,B,A>的長度。對於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”。
算法分析:
由於每次調用至少向上或向左(或向上向左同時)移動一步,故最多調用(m + n)次就會遇到i = 0或j = 0的情況,此時開始返回。返回時與遞歸調用時方向相反,步數相同,故算法時間復雜度為Θ(m + n)。
#include <stdio.h> #include <string.h> #define MAXLEN 100 void LCSLength(char *x, char *y, int m, int n, int c[][MAXLEN], int b[][MAXLEN]) { int i, j; for (i = 0; i <= m; i++) c[i][0] = 0; for (j = 1; j <= n; j++) c[0][j] = 0; for (i = 1; i <= m; i++) { for (j = 1; j <= n; j++) { if (x[i - 1] == y[j - 1]) { c[i][j] = c[i - 1][j - 1] + 1; b[i][j] = 0; } else if (c[i - 1][j] >= c[i][j - 1]) { c[i][j] = c[i - 1][j]; b[i][j] = 1; } else { c[i][j] = c[i][j - 1]; b[i][j] = -1; } } } } void PrintLCS(int b[][MAXLEN], char *x, int i, int j) { if (i == 0 || j == 0) return; if (b[i][j] == 0) { PrintLCS(b, x, i - 1, j - 1); printf("%c ", x[i - 1]); } else if (b[i][j] == 1) PrintLCS(b, x, i - 1, j); else PrintLCS(b, x, i, j - 1); } int main(int argc, char **argv) { char x[MAXLEN] = { "ABCBDAB" }; char y[MAXLEN] = { "BDCABA" }; int b[MAXLEN][MAXLEN]; int c[MAXLEN][MAXLEN]; int m, n; m = strlen(x); n = strlen(y); LCSLength(x, y, m, n, c, b); PrintLCS(b, x, m, n); return 0; }
分兩個部分:(參考:http://blog.csdn.net/v_july_v/article/details/6695482)
1)計算最優值 LCSLength()
計算最長公共子序列長度的動態規划算法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]中。
由算法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)構造最長公共子序列 PrintLCS()
在算法LCS中,每一次的遞歸調用使i或j減1,因此算法的計算時間為O(m+n)。
算法的改進:參考:http://blog.csdn.net/v_july_v/article/details/6695482
變形題型:參考:http://www.cnblogs.com/zhangchaoyang/articles/2012070.html(最大子序列、最長遞增子序列、最長公共子串、最長公共子序列、字符串編輯距離)
/** * 第一步:論證是否是動態規划問題 * 首先要證明最長公共子序列問題是動態規划問題,即符合動態規划算法的兩個特點:最優子結構和重疊子問題 * 最優子結構: * 記:Xi=﹤x1...xi﹥即X序列的前i個字符 (1≤i≤m)(前綴) Yj=﹤y1...yj﹥即Y序列的前j個字符 (1≤j≤n)(前綴) * 假定Z=﹤z1...zk﹥∈LCS(X , Y)。 * 若xm=yn(最后一個字符相同),則問題化歸成求Xm-1與Yn-1的LCS(LCS(X , Y)的長度等於LCS(Xm-1 , Yn-1)的長度加1)。 * 若xm≠yn,則問題化歸成求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,故問題具有最優子結構性質。 * 重疊子問題: * 在計算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>。建立遞歸關系如下: * 0 if i=0||j=0 * c[i][j]= c[i-1][j-1]+1 if i,j>0&&x[i]==y[j] * max(c[i][j-1],c[i-1][j]) if i,j>0&&x[i]!=y[j]; */ public class LCS { /**第三步:計算最優值 * 計算最長公共子序列長度的動態規划算法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]中。 * 在這里可以將數組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)時間。 * @param x * @param y * @return */ public int[][] lcsLength(char x[],char y[]){ int m = x.length; int n = y.length; int [][]c = new int[m+1][n+1]; for(int i = 0;i<m+1;i++) c[i][0]=0; for(int j = 0;j<n+1;j++) c[0][j]=0; for(int i = 1;i<=m;i++){ for(int j = 1;j<=n;j++){ //i,j從1開始,所以下面用i-1和j-1使得可以從數組0元素開始 if(x[i-1]==y[j-1]){ c[i][j] = c[i-1][j-1]+1; }else if(c[i-1][j]>=c[i][j-1]){ c[i][j]=c[i-1][j]; }else{ c[i][j]=c[i][j-1]; } } } return c; } /**第四步:構造最長公共子序列 * lcs函數用來構造最長公共子序列,它使用在計算最優值中得到的c數組可以快速的 * 構造序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最長公共子序列。 * @param c * @param x * @param i * @param j */ public void lcs(int c[][],char x[],int i,int j){ if(i==0||j==0)return; if(c[i][j]==(c[i-1][j-1]+1)){ lcs(c,x,i-1,j-1); //注意c的長度要比x大1 System.out.println(x[i-1]); }else if(c[i][j]==c[i-1][j]){ lcs(c,x,i-1,j); }else{ lcs(c,x,i,j-1); } } //測試 public static void main(String args[]){ char x[]={'A','B','C','B','D','A','B'}; char y[]={'B','D','C','A','B','A'}; LCS lcs = new LCS(); int [][]c = lcs.lcsLength(x, y); lcs.lcs(c, x, x.length, y.length); } }
參考:
http://www.cnblogs.com/zhangchaoyang/articles/2012070.html
http://blog.csdn.net/yysdsyl/article/details/4226630
