LIS最長公共子序列(動態規劃與遞歸的解法博弈)

動態規劃一 最長公共子序列

考慮假如有如下兩個字符串 求其最長的公共子序列
FRAME
FAMILY

根據最長公共子序列的定義,顯然一眼可以看出最長子串爲: FAM

那麼要怎麼麼求呢?怎麼用代碼實現呢?

在這裏首先分析一下求法。
比如給字符串一個序號一個串叫A串(FRAME),一個叫B串(FAMILY)

A串:
|   F    |    R      |   A       |   M      |   E     |
B串:
|   F    |    A      |   M       |   I      |   L     |    Y    |

首先可以考慮採用遞歸來解決

step1: 考慮首字符F相等, 那麼可以去掉F子序列長度加一

然後問題就變成了求A串(RAME),B串(AMILY)最長子串問題。

A串:
|    R      |   A       |   M      |   E     |
B串:
|    A      |   M       |   I      |   L     |    Y    |
step2: 那如果不相等呢?
  1. 一種方法是繼續求A串和B串。(這裏顯然是不行的, 因爲繼續求得結果還是不相等)
  2. 另一種方法就是:去掉A串與B串不相等的首字母(R)
    使得問題變爲:
A串:
|   A       |   M      |   E     |
B串:
|    A      |   M       |   I      |   L     |    Y    |
  1. 還一種方法就是:去掉B串與A串不相等的首字母(A)
    使得問題變爲:
A串:
|    R      |   A       |   M      |   E     |
B串:
|    M      |   I       |   L      |    Y    |

一眼看去並不知道2和3哪種方法求出的結果更好。於是取其中最大的那個,即爲所求。

那到這裏問題分析完了, 給代碼吧!

/*
 * 	最長公共子序列	
 */
public class test_1 {

	static int 最長公共子序列_遞歸(String A, String B){
		// 如果有一個字符串長度爲0 那麼公共子序列長度爲0
		if(A.length() == 0 || B.length() == 0) {
			return 0;
		}
		if(A.charAt(0) == B.charAt(0)) {// 如果首字符相等
			// 求解子問題, 主意尾部要加一(字符相等表示爲子序列長度加 1)
			return 最長公共子序列_遞歸(A.substring(1), B.substring(1)) + 1;
		}else {// 如果字符不相等
			// 取2和3其中最大的那個
			return Math.max(最長公共子序列_遞歸(A, B.substring(1)), 
				最長公共子序列_遞歸(A.substring(1), B));
		}
	}
	public static void main(String[] args) {
		// 定義兩個字符串
		String A = "FRAME";
		String B = "FAMILY";
		int num = 最長公共子序列_遞歸(A, B);
		System.out.println("最長公共子序列長度爲:" + num);
	}
	
}

遞歸時間複雜度

從代碼中不難發現此遞歸的時間複雜度非常的高。近似於2的N次方
而時間複雜度主要來源在於這一行代碼。

return Math.max(最長公共子序列_遞歸(A, B.substring(1)), 
				最長公共子序列_遞歸(A.substring(1), B));

我們知道2的N次方是一個爆炸性數量級。當A串和B串長度達到10幾可能就需要好幾秒才能求出。
那有沒有一種更好的方法呢?
其實你再分析上面考慮的幾種情況。

  1. 當字符相等時
  2. 當字符不相等時

假如將兩個字符串分別放在一張圖的X軸和Y軸上。

  1. 當字符相等時剪切A和B時所得的結果(也就是左上角 + 1)即可。
  2. 當字符不相等時, 取分別減掉A和減掉B的結果的最大值(左邊 和 上邊最大值)即可。

於是可以初始化矩陣爲:

思想解決了,那麼代碼就很簡單了。
上代碼吧!

// 注: 這裏就不再寫主函數了, 此函數可以直接調用運行
/**
	 * 	求最長公共子序列 - 動態規劃算法
	 * @param str1 A串
	 * @param str2 B串
	 * @return
	 */
	public static int LCS(String str1, String str2) {
		// 獲取字符串長度
		int str1Len = str1.length();
		int str2Len = str2.length();
		// 創建初始矩陣
		int myMap[][] = new int[str1Len + 1][str2Len + 1];
		// 記錄最長字符串的來源// 上面爲2,	 左邊爲3, 對角爲1 
		int d[][] = new int[str1Len + 1][str2Len + 1];
		// 開始遍歷計算
		for(int i = 1; i < str1Len + 1; i++) {
			for(int j = 1; j < str2Len + 1; j++) {
				// 如果兩個字符相等, 那麼就取上一次匹配加一
				if(str1.charAt(i - 1) == str2.charAt(j - 1)) {
					myMap[i][j] = myMap[i - 1][j - 1] + 1;
					// 記錄路徑
					d[i][j] = 1;
				}else {
					// 否則, 就取剪掉第一個串,與第二個串的最大值計算的
					if(myMap[i - 1][j] > myMap[i][j - 1]) {
						myMap[i][j] = myMap[i - 1][j];
						d[i][j] = 2;
					}else {
						d[i][j] = 3;
						myMap[i][j] = myMap[i][j - 1];
					}
				}
			}
		}
		/////////////////////////////////////////  計算結束
		// 輸出路徑矩陣
		System.out.println("路徑矩陣D爲: ");
		for(int i = 0; i < str1Len + 1; i++) {
			for(int j = 0; j < str2Len + 1; j++) {
				System.out.print(d[i][j] + " ,");
			}
			System.out.println();
		}
		// 輸出最長子串// 通過路徑矩陣可以逆向找出最長子序列是哪幾個字符構成的
		int tFind = 10;
		int i = str1Len;
		int j = str2Len;
		StringBuffer LSCStr = new StringBuffer("");
		while(tFind != 0) {
			tFind = d[i][j];
			if(tFind == 3) {
				// 來源爲左邊
				j--;
			}else if(tFind == 2) {
				// 來源爲上邊
				i--;
			}else if(tFind == 1) {
				LSCStr.append(str2.charAt(j - 1));
				i--;
				j--;
			}
		}
		// 輸出最長序列// 由於是逆向構造出的, 所以要翻轉一下字符串
		System.out.println("最大子序列爲: " + LSCStr.reverse());
		System.out.println("最大子序列長度爲: " + myMap[str1Len][str2Len]);
		// 返回最大子序列長度
		return myMap[str1Len][str2Len];
	}

動態規劃時間複雜度

從代碼中不難發現動態規劃的時間複雜度相較與遞歸減小了很多。
近似於N*M(N爲A串長度, M爲B串長度)
而時間複雜度主要來源在於這兩行代碼。

for(int i = 1; i < str1Len + 1; i++) {
			for(int j = 1; j < str2Len + 1; j++) {

空間複雜度

N*M(用了兩個二維數組來存放路徑來源和結構矩陣。)

不難看出動態規劃能夠較好的解決這一問題。

此文到此結束。謝謝閱讀。歡迎評論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章