最長公共子序列問題和動態規劃
最長子序列問題
子序列定義
可以注意到,子序列不要求所選的字母連續,只要求是按原次序組成就好
這是和子串的一個區別
最長公共子序列定義
最長公共子序列(L ongest C ommon S equence)
簡稱為LCS,下同
直觀明瞭,就是兩個序列的共有的子序列中最長的一個,
此圖裡就是 DATA這一個單詞
解法
1. 暴力法
首先我們想到的便是把兩個序列的所有可能的子序列枚舉出來,一一進行比較.
所有一個序列的子序列的組合有 種可能,而且需要m次比較.
所以時間複雜度是O(),空間複雜度是O(2^n);
顯然出現了 指數形式的複雜度,這是在時間和空間上無法接受的.
2 遞迴
對於序列A[0,N] 和A[0,M];
他們的最長自序列LCS(A,B)有三種情況
①. 最後一個字母相等,直接將其剔除
②.末子符不等
這有點不好理解,其實在開始遞迴時 ,程式並不知道誰能取得更大的字串,
所以將分別對應的兩種情況都進行遞迴直到遞迴出口,(相當於將每種情況都走完)
之後把所有的情況每次都層層返回,
每次返回都進行一次比較,總是取最大的返回值,這樣就得到了更長者
③.遞迴出口
當序列為空的時候,返回0;
總結下來就是如下公式
根據這個公式很容易得出遞迴版的程式碼
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
char a[30], b[30];
int a_len, lenb;
int LCS(int, int);
int main()
{
strcpy(a, "ABCBDAB");
strcpy(b, "BDCABA");
a_len = strlen(a);
b_len = strlen(b);
printf("%d\n", LCS(a_len - 1, b_len - 1));//從末尾開始遞迴
getchar();
return 0;
}
int LCS(int i, int j)
{
if (i <0|| j <0)
return 0;
if (a[i] == b[j])
return 1 + LCS(i -1, j-1);
else
return LCS(i -1, j)>LCS(i, j - 1) ? LCS(i - 1, j) : LCS(i, j - 1);
}
考慮演算法的複雜度
時間複雜度在最好的情況下
例如,兩個序列相等,只需要O(n)線性時間內完成,
但若在最壞的情況下,
每進行一次遞迴,就會引發新的兩個問題,
而新的兩個問題又會引發各自新的問題,
重點是在各自的問題中會出現大量重複的問題.導致時間複雜度激增.
為O()
最壞情況下的遞迴例子如圖
動態規劃求解
我們提到,用遞迴解決此問題最大一個問題就是:
當不是最優解時總會出現重複計算的遞迴
為了解決這個問題,首先想到可以用一張表存入已計算的資料,
在計算之前先查表需要計算的資料是否存在,若不存在則再計算.
這是一種以空間複雜度換取時間複雜度的做法.
出現重複遞迴的本質是在每一次遞迴的不確定性,當末序列不相同時,是向右走還是向上走是未知的.如何具體確定路徑呢?
我們可以從頭開始畫一張表
每個方塊代表問題的解
從左往右,從上到下一次將這張表填滿.規則如下
當然會出現多解和歧義解的情況,但不在本問題討論之內.
這張表就表示了所有可能出現的情況,從左上角或者右下角開始進行推到,很容易得到正確的結果.而且不會重複計算
以從右下角為例
總能取出一個最長子序列BCBA
下面是程式碼
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
char a[500], b[500];
char num[501][501]; ///記錄中間結果的陣列
char flag[501][501]; ///標記陣列,用於標識下標的走向,構造出公共子序列
void LCS(); ///動態規劃求解
void getLCS(); ///採用倒推方式求最長公共子序列
int main()
{
int i;
strcpy(a, "CDEFG");
strcpy(b, "BDCABA");
memset(num, 0, sizeof(num));
memset(flag, 0, sizeof(flag));
LCS();
printf("%d\n", num[strlen(a)][strlen(b)]);
getLCS();
getchar();
return 0;
}
void LCS()
{
int i, j;
for (i = 1; i <= strlen(a); i++)
{
for (j = 1; j <= strlen(b); j++)
{
if (a[i - 1] == b[j - 1]) ///注意這裡的下標是i-1與j-1
{
num[i][j] = num[i - 1][j - 1] + 1;
flag[i][j] = 1; ///對角線
}
else if (num[i][j - 1]>num[i - 1][j])
{
num[i][j] = num[i][j - 1];
flag[i][j] = 2; ///向左
}
else
{
num[i][j] = num[i - 1][j];
flag[i][j] = 3; ///向上
}
}
}
}
void getLCS()
{
char res[500];
int i = strlen(a);
int j = strlen(b);
int k = 0; ///用於儲存結果的陣列標誌位
while (i>0 && j>0)
{
if (flag[i][j] == 1) ///如果是對角線標記
{
res[k] = a[i - 1];
k++;
i--;
j--;
}
else if (flag[i][j] == 2) ///如果是向左標記
j--;
else if (flag[i][j] == 3) ///如果是向上標記
i--;
}
for (i = k - 1; i >= 0; i--)
printf("%c", res[i]);
}