天天看點

Python-求解兩個字元串的最長公共子

一、問題描述

    給定兩個字元串,求解這兩個字元串的最長公共子序列(Longest Common Sequence)。比如字元串1:BDCABA;字元串2:ABCBDAB。則這兩個字元串的最長公共子序列長度為4,最長公共子序列是:BCBA

二、算法求解

這是一個動态規劃的題目。對于可用動态規劃求解的問題,一般有兩個特征:①最優子結構;②重疊子問題

①最優子結構

設X=(x1,x2,...,xn)和Y=(y1,y2,...,ym)是兩個序列,将X和Y的最長公共子序列記為LCS(X,Y)

找出LCS(X,Y)就是一個最優化問題。因為,我們需要找到X和Y中最長的那個公共子序列。而要找X和Y的LCS,首先考慮X的最後一個元素和Y的最後一個元素。

⑴如果xn=ym,即X的最後一個元素與Y的最後一個元素相同,這說明該元素一定位于公共子序列中。是以,現在隻需要找:LCS(Xn-1,Ym-1)

LCS(Xn-1,Ym-1)就是原問題的一個子問題。為什麼叫子問題?因為它的規模比原問題小。

為什麼是最優的子問題?因為我們要找的是Xn-1和Ym-1的最長公共子序列啊。最長的!換句話說就是最優的那個。

⑵如果xn!=ym,這下要麻煩一點,因為它産生了兩個子問題:LCS(Xn-1,Ym)和LCS(Xn,Ym-1)

因為序列X和序列Y的最後一個元素不相等,那說明最後一個元素不可能是最長公共子序列中的元素。

LCS(Xn-1,Ym)表示:最長公共序列可以在(x1,x2,...xn-1)和(y1,y2,...,ym)中找。

LCS(Xn,Ym-1)表示:最長公共序列可以在(x1,x2,...xn)和(y1,y2,...,ym-1)中找。

求解上面兩個子問題,得到的公共子序列誰最長,那誰就是LCS(X,Y)。用數學表示就是:

LCS=max{LCS(Xn-1,Ym),LCS(Xn,Ym-1)}

由于條件⑴和⑵考慮到了所有可能的情況。是以,我們成功的把原問題轉化成了三個規模更小的問題。

②重疊子問題

重疊子問題是什麼?就是說原問題轉化成子問題後,子問題中有相同的問題。

原問題是:LCS(X,Y)。子問題有❶LCS(Xn-1,Ym-1)❷ LCS(Xn-1,Ym)❸ LCS(Xn,Ym-1)

乍一看,這三個問題是不重疊的。可本質上它們是重疊的,因為它們隻重疊了一大部分。舉例:

第二個子問題:LCS(Xn-1,Ym)就包含了問題❶LCS(Xn-1,Ym-1),為什麼?

因為,當Xn-1和Ym的最後一個元素不相同時,我們又需要将LCS(Xn-1,Ym-1)進行分解:分解成:LCS(Xn-1,Ym-1)和LCS(Xn-2,Ym)

也就是說:在子問題的繼續分解中,有些問題是重疊的。

由于像LCS這樣的問題,它具有重疊子問題的性質,是以:用遞歸來求解就太不劃算了。國為采用遞歸,它重複地求解了子問題,而且需要注意的是,所有子問題加起來的個數是指數級的。

那麼問題來了,如果用遞歸求解,有指數級個子問題,故時間複雜度是指數級的。這指數級個子問題,難道用了動态規劃,就變成多項式時間了??

關鍵是采用動态規劃時,并不需要去一一計算那些重疊了的子問題。或者說:用了動态規劃之後,有些子問題是通過“查表”直接得到的,而不是重新又計算一遍得到的。舉個例子:比如求Fib數列。

Python-求解兩個字元串的最長公共子

求fib(5),分解成了兩個子問題:fib(4)和fib(3),求解fib(4)和fib(3)時,又分解了一系列的小問題...

從圖中可以看出:根的左右子樹:fib(4)和fib(3)下,是有很多重疊的!比如,對于fib(2),它就一共出現了三次。如果用遞歸來求解,fib(2)就會被計算三次,而用DP(Dynamic Programming)動态規劃,則fib(2)隻會計算一次,其他兩次則是通過“查表”直接求得。而且,更關鍵的是:查找求得該問題的解之後,就不需要再繼續去分解該問題了。而對于遞歸,是不斷地将問題解,直到分解為基準問題(fib(0)或者fib(1))

說了這麼多,還是寫下最長公共子序列的遞歸式才完整。

Python-求解兩個字元串的最長公共子

C[i,j]表示:(x1,x2,...,xi)和(y1,y2,...,yj)的最長公共子序列的長度。公式的具體解釋可參考《算法導論》動态規劃章節

三、LCS Python代碼實作

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/5/16
# Name     : test03
# Software : PyCharm
# Note     : 用于實作求解兩個字元串的最長公共子序列


def longestCommonSequence(str_one, str_two, case_sensitive=True):
    """
    str_one 和 str_two 的最長公共子序列
    :param str_one: 字元串1
    :param str_two: 字元串2(正确結果)
    :param case_sensitive: 比較時是否區分大小寫,預設區分大小寫
    :return: 最長公共子序列的長度
    """
    len_str1 = len(str_one)
    len_str2 = len(str_two)
    # 定義一個清單來儲存最長公共子序列的長度,并初始化
    record = [[0 for i in range(len_str2 + 1)] for j in range(len_str1 + 1)]
    for i in range(len_str1):
        for j in range(len_str2):
            if str_one[i] == str_two[j]:
                record[i + 1][j + 1] = record[i][j] + 1
            elif record[i + 1][j] > record[i][j + 1]:
                record[i + 1][j + 1] = record[i + 1][j]
            else:
                record[i + 1][j + 1] = record[i][j + 1]

    return record[-1][-1]

if __name__ == '__main__':
    # 字元串1
    s1 = "BDCABA"
    # 字元串2
    s2 = "ABCBDAB"
    # 計算最長公共子序列的長度
    res = longestCommonSequence(s1, s2)
    # 列印結果
    print(res) # 4           

複制