動態規劃一 最長公共子序列
考慮假如有如下兩個字符串 求其最長的公共子序列
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: 那如果不相等呢?
- 一種方法是繼續求A串和B串。(這裏顯然是不行的, 因爲繼續求得結果還是不相等)
- 另一種方法就是:去掉A串與B串不相等的首字母(R)
使得問題變爲:
A串:
| A | M | E |
B串:
| A | M | I | L | Y |
- 還一種方法就是:去掉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幾可能就需要好幾秒才能求出。
那有沒有一種更好的方法呢?
其實你再分析上面考慮的幾種情況。
- 當字符相等時
- 當字符不相等時
假如將兩個字符串分別放在一張圖的X軸和Y軸上。
- 當字符相等時剪切A和B時所得的結果(也就是左上角 + 1)即可。
- 當字符不相等時, 取分別減掉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(用了兩個二維數組來存放路徑來源和結構矩陣。)
不難看出動態規劃能夠較好的解決這一問題。
此文到此結束。謝謝閱讀。歡迎評論。