1. 程式人生 > >動態規劃演算法--解最長公共子序列問題

動態規劃演算法--解最長公共子序列問題

如今最好,沒有來日方長!

一、簡述動態規劃演算法

1.動態規劃演算法簡介

(1)背景

動態規劃(英語:Dynamic programming,簡稱:DP)是一種演算法設計技術,是運籌學的一個分支,是求解決策過程(Decision process)最優化的數學方法。值得注意的是,這個技術名字中的“programming”是計劃和規劃的意思,不是計算機領域中的程式設計。它有著有趣的歷史。
它由20世紀50年代初美國一位卓越的數學家Richard Bellman所發明。數學家們在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),把多階段過程轉化為一系列單階段問題逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。

(2)基本思想

動態規劃演算法通常用於求解最優決策的問題。其基本思想是基於初始狀態,將待求解問題分解成若干個子問題,其中當前子問題的解由上一次子問題的解推出,然後從這些子問題的解得到原問題的解。具體的動態規劃演算法有很多種,它們都用一個表來記錄所有已解子問題的答案,以此避免重複計算,而節約時間。

(3)例項分析理解

這裡寫圖片描述
這裡寫圖片描述
假設情景:某一天中午起床晚了,十二棟到惟義樓w7504的路
路線很多,只有走最短路線才能避免遲到。路線及所花時間,如上圖所示,求解最短路線
應用動態規劃演算法的理解:將該過程分為A.B.C.D四階段,每個階段中,都求出本階段的各個初始狀態到過程終點的最短路徑和最短距離,逆序倒推到起點十二棟時,便得到十二棟到教室的最短路徑及最短距離。

2.演算法特點

顯而易見,動態規劃演算法可以很好的求解最優化問題。可以有效地解決冗餘,降低原來具有指數級複雜度演算法的時間複雜度。
最大的特點就是可以求得最優策略,符合最優化原理,即“最優策略的子策略也是最優策略”。
但是,任何思想方法都有一定的侷限性。
因此,動態規劃也有一些適用條件,即:適用動態規劃的問題必須滿足最優化原理和無後效性。簡單說明,最優化原理即:一個最優化策略的子策略總是最優的;無後向性即:每個階段的歷史狀態無法直接影響它的未來決策,只能通過當前的這個狀態做出決策。例如,我在階段B的某位置,階段C“圖書館”以及“二食堂”的位置不會影響我選擇哪條路。

3.動態規劃演算法應用

動態規劃演算法普遍在數學、經濟學、電腦科學中使用。在最優控制、生產排程、機器學習等方面得到了廣泛的應用。例如最短路線、庫存管理、資源分配、裝置更新、排序、揹包問題、影象資料壓縮、手勢識別、語音識別、資料探勘、資訊檢索以及最長公共子序列等應用問題。

二、問題求解

1.演算法設計與論證

採用動態規劃演算法思想求解最長公共子序列,先將問題分解成若干個子問題,再求解子問題的最優子策略,最後求解原問題的最優策略。

(1)基本概念

在分析最長公共子序列的最優子結構前,先清楚以下概念:
子序列:某個序列的子序列指從原序列中不破壞順序地任意除去若干個字元後形成的序列。
例:序列X,Y,其中X=“BABBCDB”,Y=“BDB”,則Y是X的一個子序列。

公共子序列:如果Z是X的子序列也是Y的子序列,那麼Z是序列X與Y的公共子序列

最長公共子序列結構性質:第i字首
如序列A=(a1,a2,…..,am),則Ai=(a1,….,ai-1,ai)是序列A的第i字首。
例:A=(A,B,D,C,D),A1=(A),A2=(A,B),A3=(A,B,D)

(2)最優子結構

由最長公共子序列結構性質可分析得最優子結構,現如題,假設段落一為序列A=(a1,a2,…..,am)(設長度為m),段落二為序列T=(t1,t2,…,tn)(設長度為n),A與T的最長公共子序列為S=(s1,s2,….,sk)(設長度為k)則:
若am=tn,則sk=am=tn且Sk-1是Am-1和Tn-1的最長公共子序列;
若am≠tn且sk≠am ,則S是Am-1和T的最長公共子序列;
若am≠tn且sk≠tn ,則S是A和Tn-1的最長公共子序列
根據最長公共子序列的最優子結構可知,當am=tn時,A,T的最長公共子序列即Am-1和Tn-1的最長公共子序列加上am,當am≠tn時,A,T的最長公共子序列即Am-1和T的最長公共子序列或者A和Tn-1的最長公共子序列,則A和T的最長公共子序列為這兩個最長公共子序列的最長者。
因此,求解A,T的最長公共子序列問題,均包括子問題,求解Am-1,Tn-1的最長公共子序列。

(3)最優值

由最長公共子序列的最優子結構得最長公共子序列最優值,可建立如下公式,用c[i,j]表示序列Ai和Tj的最長公共子序列的長度,則:
c[i,j] = 0, i=0或j=0
c[i,j] = c[i-1,j-1]+1, ai=tj
c[i,j] = max(c[i,j-1],c[i-1,j], ai≠tj

虛擬碼:

Description:The longest common sub-sequence problem . 
Init: m,d {m,d:(L1+1)(L2+1)的0矩陣}
Input: s1,s2,L1,L2
Function LCS(s1,s2)
L1=length[s1];L2=length[s2]
FOR p1=1 TO L1
FOR p2=1 TO L2
            IF  s1[p1] == s2[p2]                             
m[p1+1][p2+1] = m[p1][p2]+1  
           d[p1+1][p2+1] = 1           
        IF m[p1][p2+1] >= m[p1+1][p2]                
m[p1+1][p2+1] = m[p1][p2+1]   
            d[p1+1][p2+1] = 2           
        ELSE                                        
        m[p1+1][p2+1] = m[p1+1][p2]     
            d[p1+1][p2+1] = 3           
            ENDIF
         ENDIF
    ENDFOR 
ENDFOR 
Output: m,d

程式碼實現:

import numpy as np
#np.set_printoptions(threshold = 1e6)#設定列印數量閾值,避免列印省略號
def LCS(s1, s2):   
    # 序列長度加1的0矩陣
    # m用來儲存對應位置匹配的結果  
    m = [ [ 0 for x in range(len(s2)+1) ] for y in range(len(s1)+1) ]   
    # d用來記錄回溯矩陣 
    d = [ [ 0 for x in range(len(s2)+1) ] for y in range(len(s1)+1) ]   
    for p1 in range(len(s1)):   
        for p2 in range(len(s2)):   
            if s1[p1] == s2[p2]:              #匹配成功,取左上方的值加1,並標記回溯數字1  
                m[p1+1][p2+1] = m[p1][p2]+1  
                d[p1+1][p2+1] = 1           
            elif m[p1][p2+1] >= m[p1+1][p2]:  #左邊值大於上邊值,則取左值,並標記回溯數字2  
                m[p1+1][p2+1] = m[p1][p2+1]   
                d[p1+1][p2+1] = 2           
            else:                             #上值大於左值,則該位置的值為上值,並標記回溯數字3
                m[p1+1][p2+1] = m[p1+1][p2]     
                d[p1+1][p2+1] = 3         
    p1, p2 = len(s1), len(s2)
    print(m[p1][p2])
    #print(np.array(m))                        #列印對應位置匹配矩陣
    #print (np.array(d))                       #列印回溯矩陣 
    #回溯 構造最長公共子序列
    s = ''                  #儲存最長公共子序列
    while m[p1][p2]:        #不為0時  
        c = d[p1][p2]
        if c == 1:          #匹配成功,插入1,並向左上角找下一個  
            s=s1[p1-1]+s
            p1-=1  
            p2-=1   
        if c ==2:           #根據標記,向左找下一個  
            p1 -= 1  
        if c == 3:          #根據標記,向上找下一個  
            p2 -= 1    
    return s

具體演算法語言描述:
用矩陣記錄兩個基因序列的匹配程度,如果匹配則左上方的數字+1,否則取左邊和上邊的最大值,如果左邊值大於上邊值,則取左邊值並標記回溯數字“3”,如果上邊值大於左邊值,則取上邊值並標記回溯數字“2”,如果左邊值等於上邊值,則取上邊值並標記回溯數字“2”。

測試:

if __name__ == '__main__':   

    x = 'ACAAGATGCCATTGTCC'
    y = 'TATAAATGTGTACA'
    print(LCS(x,y))

匹配矩陣:

[[ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  1  1  1  1  1  1  1  1  1  1  1  1  1]
 [ 0  0  1  1  1  1  1  1  1  1  1  1  1  2  2]
 [ 0  0  1  1  2  2  2  2  2  2  2  2  2  2  3]
 [ 0  0  1  1  2  3  3  3  3  3  3  3  3  3  3]
 [ 0  0  1  1  2  3  3  3  4  4  4  4  4  4  4]
 [ 0  0  1  1  2  3  4  4  4  4  4  4  5  5  5]
 [ 0  1  1  2  2  3  4  5  5  5  5  5  5  5  5]
 [ 0  1  1  2  2  3  4  5  6  6  6  6  6  6  6]
 [ 0  1  1  2  2  3  4  5  6  6  6  6  6  7  7]
 [ 0  1  1  2  2  3  4  5  6  6  6  6  6  7  7]
 [ 0  1  2  2  3  3  4  5  6  6  6  6  7  7  8]
 [ 0  1  2  3  3  3  4  5  6  7  7  7  7  7  8]
 [ 0  1  2  3  3  3  4  5  6  7  7  8  8  8  8]
 [ 0  1  2  3  3  3  4  5  6  7  8  8  8  8  8]
 [ 0  1  2  3  3  3  4  5  6  7  8  9  9  9  9]
 [ 0  1  2  3  3  3  4  5  6  7  8  9  9 10 10]
 [ 0  1  2  3  3  3  4  5  6  7  8  9  9 10 10]]

回溯矩陣

[[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
 [0 2 1 3 1 1 1 3 3 3 3 3 1 3 1]
 [0 2 2 2 2 2 2 2 2 2 2 2 2 1 3]
 [0 2 1 2 1 1 1 3 3 3 3 3 1 2 1]
 [0 2 1 2 1 1 1 3 3 3 3 3 1 3 1]
 [0 2 2 2 2 2 2 2 1 3 1 3 3 3 3]
 [0 2 1 2 1 1 1 3 2 2 2 2 1 3 1]
 [0 1 2 1 2 2 2 1 3 1 3 1 2 2 2]
 [0 2 2 2 2 2 2 2 1 3 1 3 3 3 3]
 [0 2 2 2 2 2 2 2 2 2 2 2 2 1 3]
 [0 2 2 2 2 2 2 2 2 2 2 2 2 1 2]
 [0 2 1 2 1 1 1 2 2 2 2 2 1 2 1]
 [0 1 2 1 2 2 2 1 2 1 3 1 2 2 2]
 [0 1 2 1 2 2 2 1 2 1 2 1 3 3 2]
 [0 2 2 2 2 2 2 2 1 2 1 2 2 2 2]
 [0 1 2 1 2 2 2 1 2 1 2 1 3 3 3]
 [0 2 2 2 2 2 2 2 2 2 2 2 2 1 3]
 [0 2 2 2 2 2 2 2 2 2 2 2 2 1 2]]

最長公共子序列及長度

10
AAAATGTGTC

也可以用來檢測文件間的匹配度,只需要增加以下內容即可:

def loadData(filename):
    import jieba
    txt = open(filename).read()
    for char in '!,。·~、?‘;“:”+-|、||《》{}【】&*()-&……%':
        words = jieba.lcut(txt)
        words = txt.replace(char," ")     #噪聲處理,將特殊字元替換為空格

    return words


if __name__ == '__main__': 
    txt1 = loadData('三生三世十里桃花.txt')
    txt2 = loadData('桃花債.txt')
    print(LCS(txt2,txt1))

三、演算法複雜度分析及優化

對於一個具體的問題,設計與分析得出的演算法,往往考慮兩點,一即演算法正確性;二即演算法的時間及空間複雜度。

動態規劃演算法求最長公共子序列問題的時間複雜度為和空間複雜度均為:Ο(mn)

常見的演算法時間複雜度由小到大依次為:Ο(1)<Ο(log2n)<Ο(n)<O(nlog2n)<Ο(n²)<Ο(n³)<…<Ο(2ⁿ)<Ο(n!)
則Ο(n²)<Ο(mn)<Ο(n³),如下圖所示,當n越大所需時間隨指數級增長。
這裡寫圖片描述

複雜度與計算時間關係如下圖,雖然動態規劃演算法有效地降低了指數級增長的複雜度,但當資料較大時,複雜度仍舊遞增增長,查資料顯示可以將此問題時間複雜度降低到Ο(nlogn),由於目前我的演算法優化程度不夠,所以,這裡提取《三生三世十里桃花》前三章的內容以及《桃花債》前十章的內容,進行求解其最長公共子序列的長度。

這裡寫圖片描述

去除矩陣d後優化程式碼如下:

import numpy as np
def LCS(s1, s2):   
    # 序列長度加1的0矩陣
    # m用來儲存對應位置匹配的結果  
    m = [ [ 0 for x in range(len(s2)+1) ] for y in range(len(s1)+1) ]     
    for p1 in range(len(s1)):   
        for p2 in range(len(s2)):   
            if s1[p1] == s2[p2]:              #匹配成功,取左上方的值加1  
                m[p1+1][p2+1] = m[p1][p2]+1             
            else:                             #取左邊值和上邊值的大值
                m[p1+1][p2+1] = max(m[p1+1][p2], m[p1][p2+1])    
    p1, p2 = len(s1), len(s2)
    return m[p1][p2]

def loadData(filename):
    import jieba
    txt = open(filename).read()
    words = jieba.lcut(txt) #讀取中文字元
    return words


if __name__ == '__main__': 
    txt1 = loadData('三生三世十里桃花.txt')
    txt2 = loadData('桃花債.txt')
    print(LCS(txt2,txt1)) 

四、總結

該演算法還可以進一步優化,如果演算法沒有進一步優化到對數級別上,最好不要嘗試100000的資料,會宕機的……