1. 程式人生 > >最長公共子序列問題和動態規劃

最長公共子序列問題和動態規劃

最長子序列問題

子序列定義

這裡寫圖片描述
可以注意到,子序列不要求所選的字母連續,只要求是按原次序組成就好
這是和子串的一個區別

這裡寫圖片描述

最長公共子序列定義

最長公共子序列(L ongest C ommon S equence)
簡稱為LCS,下同
這裡寫圖片描述
直觀明瞭,就是兩個序列的共有的子序列中最長的一個,
此圖裡就是 DATA這一個單詞

解法

1. 暴力法

首先我們想到的便是把兩個序列的所有可能的子序列枚舉出來,一一進行比較.

所有一個序列的子序列的組合有 2n種可能,而且需要m次比較.

所以時間複雜度是O(m2n),空間複雜度是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(2n)
最壞情況下的遞迴例子如圖
這裡寫圖片描述

動態規劃求解

我們提到,用遞迴解決此問題最大一個問題就是:
當不是最優解時總會出現重複計算的遞迴
為了解決這個問題,首先想到可以用一張表存入已計算的資料,
在計算之前先查表需要計算的資料是否存在,若不存在則再計算.
這是一種以空間複雜度換取時間複雜度的做法.
出現重複遞迴的本質是在每一次遞迴的不確定性,當末序列不相同時,是向右走還是向上走是未知的.如何具體確定路徑呢?
我們可以從頭開始畫一張表
這裡寫圖片描述
每個方塊代表問題的解
從左往右,從上到下一次將這張表填滿.規則如下
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
當然會出現多解和歧義解的情況,但不在本問題討論之內.

這張表就表示了所有可能出現的情況,從左上角或者右下角開始進行推到,很容易得到正確的結果.而且不會重複計算
以從右下角為例
這裡寫圖片描述
總能取出一個最長子序列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]);
}