動態規劃入門之最長公共子序列(LCS)
LCS是動態規劃在字串問題中應用的典型。問題描述:給定2個序列,求這兩個序列的最長公共子序列,不要求子序列連續。例如{2,4,3,1,2,1}和{1,2,3,2,4,1,2}的結果是{2,3,2,1}或者{2,4,1,2}。
思路:如果不用動態規劃去做,而用暴力法,則必須找出其中一個序列的所有子序列(LIS如果用暴力法思路也是如此),然後判斷這個子序列是不是另外一個序列的子序列。判斷一個序列是不是另一個序列的子序列可以在O(n)內解決(只需要兩個指標,然後依次比較並移動),但是怎麼去找一個序列的所有子序列呢,就是就這個集合的所有子集,這是指數級別的複雜度。如果用動態規劃去做呢?我們可以用d[i][j]表示序列a[0~i]和序列b[0~j]的最長公共子序列的長度,這是狀態;那麼狀態轉移方程:
d[i][j]=d[i-1][j-1]+1 if a[i]=b[j]; and d[i][j]=max{d[i][j-1],d[i-1][j]} if a[i] != b[j]。也就是說,如果i和j上的元素相等,那麼d[i-1][j-1]+1就是d[i][j],否則,就要看d[i-1][j]和d[i][j-1]誰大了。在這裡面,狀態用一個二維陣列表示,也就是一個矩陣。我這裡引用別人的一張圖幫助理解:
上圖中序列分別為{B,D,C,A,B,A}和{A,B,C,B,D,A,B}。裡面包含了所有的狀態。從上圖可以知道,每個狀態都可以從它的左上、左、或者上方走過來。具體的條件就是上面說的a[i]是否和b[j]相等。我們如果要求出最終的最長公共子序列,就需要得到上面的狀態圖,然後從圖的右下角,根據路徑回溯到左上角就行了。
接下來給出JAVA實現的LCS程式碼:
/** * * @author kerry * 求兩個序列的最長公共子序列 */ public class LCS { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub int[] a={2,4,3,1,2,1}; int[] b={1,2,3,2,4,1,2}; int out=lcsSolution(a,b); System.out.println(out); } /*計算狀態矩陣:複雜度為O(n)*/ public static int lcsSolution(int[] a,int[] b){ int len1=a.length; int len2=b.length; if(len1==0||len2==0)return 0; int[][] status=new int[len1+1][len2+1]; for(int i=0;i<=len1;i++){ for(int j=0;j<=len2;j++){ status[i][j]=0; } } int[][] path=new int[len1][len2]; //status[i][j]表示0~i和0~j這兩個序列的最長公共子序列的長度,也就是狀態 for(int i=0;i<=len1;i++){ for(int j=0;j<=len2;j++){ if(i==0||j==0)status[i][j]=0; //下面是狀態轉移 else if(a[i-1]==b[j-1]){ status[i][j]=status[i-1][j-1]+1;//斜方向 path[i-1][j-1]=1; } else { if(status[i][j-1]>status[i-1][j]){ status[i][j]=status[i][j-1];//左方向 path[i-1][j-1]=2; } else{ status[i][j]=status[i-1][j];//上方向 path[i-1][j-1]=3; } } } } printAns(path,a); return status[len1][len2]; } //列印結果,這裡列印的結果是反的,如果要想正向列印,可以利用遞迴的方法,類似於從尾到頭列印單鏈表的做法,也可以用棧 public static void printAns(int[][] path,int[] a){ int len1=path.length; int len2=path[0].length; int i=len1-1; int j=len2-1; while(j>=0&&i>=0){ if(path[i][j]==1){ System.out.print(a[i]+"->"); i--;j--; } else if(path[i][j]==2){ j--; } else i--; } System.out.println(); } }
為了能打印出LCS,需要繼續每次狀態轉移時候選擇的路徑,程式碼裡用path這個二維陣列記錄,最後根據這個記錄列印結果,這裡列印的結果是反的,要想正向列印可以參照程式碼中說的方法。另外:左和上這兩個方向存在優先順序關係,優先順序不同,打印出來的結果可能不同,但都是問題的解(解不唯一)。
假設序列長度分別為m和n,那麼計算矩陣的複雜度為O(m*n),列印的複雜度為 O(m+n)。
如有錯誤,請多多指出~