詳解動態規劃最長公共子序列--JavaScript實現

YinTokey發表於2018-05-28

前面兩篇我們講解了01揹包問題和最少硬幣找零問題。這篇將介紹另一個經典的動態規劃問題--最長公共子序列。如果沒看過前兩篇,可點選下面連結。

詳解動態規劃最少硬幣找零問題--JavaScript實現

詳解動態規劃01揹包問題--JavaScript實現

問題

給定兩個字串序列 abcadf , acbad,求這兩個字串的最長公共子序列

分析

最長公共子序列問題,有三個點需要注意

  • 兩個序列長度不一定相同
  • 最長子序列是指,在兩個字串序列中以相同順序出現
  • 所求的子序列不需要連續

在進行填表分析之前,根據上面提到的三個點,我們可以很容易地先直接得出答案,最長公共子序列應為 acad

1. 建表

我們給兩個子序列前面都加一個空字元,即

input1 = ["","a","c","b","a","d"],
input2 = ["","a","b","c","a","d","f"],

複製程式碼

然後構建如下表格

詳解動態規劃最長公共子序列--JavaScript實現

為什麼填一堆0呢?表示字串無法匹配,你可以理解這是一種輔助的計算方式,在分析具體子序列時,不把構建的空字元納入考慮範圍。在後面也會按照前面2篇的思路,使用T[i][j]表示組合的子序列長度。

下面將從左往右,從上往下開始填表。我們在填寫某一個表格的時候,只需要考慮小於等於i 和小於等於j的情況。比如我們要填寫T[2][2]時,那麼此時等同於求字串 ac,ab的最長公共子序列,填寫T[4][5]時,那麼此時等同於求 acbaabcad的最長公共子序列長度。

如果你看過前兩篇,對於這種填表應該會很熟悉。 下面基於這個表格,開始填表。

2. i =1

我們從第一行開始。

i=1 j=1:此時等同於求字串 aa的最長公共子序列長度,很顯然結果為1。

i=1 j=2:此時等同於求字串 aab的最長公共子序列長度,結果為1。

i=1 j=3:此時等同於求字串 aabc的最長公共子序列長度,結果為1。

只要一個序列只有一個字元,那麼另一個序列無論多長,它們的最長公共子序列長度最多隻能為1。所以 i=1 行剩餘空格都填1。

詳解動態規劃最長公共子序列--JavaScript實現

3. i = 2

i=2 j=1:此時等同於求字串 aca的最長公共子序列長度,結果為1。

i=2 j=2:此時等同於求字串 acab的最長公共子序列長度,結果為1。

i=2 j=3:此時等同於求字串 acabc的最長公共子序列長度。這時就有意思了。因為根據一開始的分析,求最長公共子序列時,子序列是可以不連續的,因此這兩個序列的最長公共子序列應該是 ac,所以這裡表格應該填2。

詳解動態規劃最長公共子序列--JavaScript實現

好了,停下,先不用急著繼續填,我們需要先分析一下通用思路。

4.填表思路

我們從T[2][3]=2 這一個格分析。很顯然去除 c 這個公共字元後,兩個字串還剩下 a, ab。是不是有點熟悉?這個其實就是填寫 T[1][2] 時的組合,也就是我們可以假設當 input1[i] == input2[j]時,T[i][j]=T[i-1][j-1]+1。 當input1[i] != input2[j]時,T[i][j]的值,取它上方或左邊的較大值,即[i][j] = max(T[i-1][j],T[i][j-1])

用一句通俗的話來描述這種T[i][j]規律,就是相等左上角加一,不等取上或左最大值,如果上左一樣大,優先取左。

好了,不看下面內容,你帶著這種規律,把表格剩餘內容自己填寫完畢。

5.最終表格

理解了這種規律,我們沒必要把每一格該怎麼填重複敘述了。下面就是最終表格。

詳解動態規劃最長公共子序列--JavaScript實現

我們舉個例子,比如 i=5 j=4,此時input1[i] !=input2[j],我們取它左邊(2)或者上方(3)的較大值,所以填寫3。

i=5 j=5,此時input1[i] ==input2[j],我們直接取左上角值加1,左上角的值為T[4][4]=3,所以T[5][5]=4 。

如果還不太理解,可以自己再練習畫一次。

6.尋找子串

我們完成填表後,只能求出最長公共子序列的長度,但是無法得知它的具體構成。我們可以參照上一篇硬幣問題,從填表的反向角度來尋找子序列。

我們子序列儲存在名為 s的陣列中,從表格中反向搜尋,找到目標字元後,每次都把目標字元插入到陣列最前面。

根據前面提供的填表口訣,我們可以反向得出尋找子序列的口訣: 如果T[i][j]來自左上角加一,則是子序列,否則向左或上回退。如果上左一樣大,優先取左。

1. 從右下角開始分析,T[5][6]=4,它並不是來自左上角。它左邊的值比上方大,所以它來自左邊,向左回退,如下圖箭頭。

詳解動態規劃最長公共子序列--JavaScript實現

2. 接著就定位到 T[5][5],顯然他來自左上角加1,它是子序列。插入陣列中,有

s = ['d']
複製程式碼

3. 扣除掉 T[5][5],可以定位到它的左上角 T[4][4],如圖:

詳解動態規劃最長公共子序列--JavaScript實現

T[4][4]也是來自左上角加1,它也是子序列,把它插入到陣列最前面,此時 s 應該是

s = ['a','d']
複製程式碼

4. 按照前面的思路,繼續定位分析,最終如下圖:

詳解動態規劃最長公共子序列--JavaScript實現
最終箭頭指向0,搜尋結束。

s = ['a','b','a','d']
複製程式碼

虛擬碼

整個分析過程已經完成了。下面提供程式碼邏輯,即使不懂 JavaScript,也不會影響你理解,因為沒有涉及語言特性。

填表

if(input1[i] == input2[j]){
	T[i][j] = T[i-1][j-1] + 1;
}else{
	T[i][j] = max(T[i-1][j],T[i][j-1])
}
複製程式碼

尋找子串

if(input1[i] == input2[j]){
	s.insertToIndexZero(input1[i]); //插入到陣列最前面
	i--;
	j--;
}else{
	//向左或向上回退
	if(T[i-1][j]>T[i][j-1]){
		//向上回退
		i--;
	}else{
		//向左回退
		j--;
	}
}
複製程式碼

完整程式碼

最終程式碼使用 JavaScript 實現,如果你的 Sublime 支援純 JavaScript,你可以直接複製黏貼程式碼,command + b 直接執行檢視結果,然後修改輸入變數,檢視更多情況下的輸出結果。

//動態規劃 -- 最長公共子序列


//!!!!  T[i][j] 計算,記住口訣:相等左上角加一,不等取上或左最大值

function longestSeq(input1,input2,n1,n2){
	var T = []; // T[i][j]表示 公共子序列長度
	for(let i=0;i<n1;i++){
		T[i] = [];
		for(let j= 0;j<n2;j++){
			if(j==0 ||i==0){
				T[i][j] = 0;
				continue;
			}
			if(input1[i] == input2[j]){
				T[i][j] = T[i-1][j-1] + 1;
			}else{
				T[i][j] = Math.max(T[i-1][j],T[i][j-1])
			}

		}

	}

	findValue(input1,input2,n1,n2,T);

	return T;

}

//!!!如果它來自左上角加一,則是子序列,否則向左或上回退。
//findValue過程,其實就是和 就是把T[i][j]的計算反過來。
function findValue(input1,input2,n1,n2,T){
	var i = n1-1,j=n2-1;
	var result = [];//結果儲存在陣列中
	console.log(i);
	console.log(j);
	while(i>0 && j>0){
		if(input1[i] == input2[j]){
			result.unshift(input1[i]);
			i--;
			j--;
		}else{
			//向左或向上回退
			if(T[i-1][j]>T[i][j-1]){
				//向上回退
				i--;
			}else{
				//向左回退
				j--;
			}
		}

	}

	console.log(result);
}


//兩個序列,長度不一定相等, 從計算表格考慮,把input1和input2首位都補一個用於佔位的空字串
var input2 = ["","a","b","c","a","d","f"],
	input1 = ["","a","c","b","a","d"],
	n1 = input1.length,
	n2 = input2.length;

console.log(longestSeq(input1,input2,n1,n2));
複製程式碼

相關文章