1. 程式人生 > >動態規劃入門之最長公共子序列(LCS)

動態規劃入門之最長公共子序列(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)。

如有錯誤,請多多指出~