【動態規劃】LCS算法 python

問題描述1

求兩字符串的連續最大公共子字符串(The Longest Common Substring)
這個LCS問題就是求兩個字符串最長公共子串的問題。解法就是用一個矩陣來記錄兩個字符串中所有位置的兩個字符之間的匹配情況,若是匹配則爲1,否則爲0。然後求出對角線最長的1的序列,其對應的位置就是最長匹配子串的位置。如圖1所示,在對角線上,連續的1就代表了兩字符串對應的位置連續相等。
在這裏插入圖片描述
從該矩陣中找到最長的全爲1的對角線,就可以找到最大公共子串,分別找出對應的字符即可求出這個最長子串。如圖1,有兩個最長的公共子串,bab與aba。

綜上所述,基本思路分爲兩步:

  1. 算一個匹配矩陣存放兩個字符串每個字符之間的匹配情況,相同爲1,不同爲0。
  2. 求這個矩陣中連續的最長的值爲1的對角線,得到這個對角線之後我們就得到了公共子串的長度和位置(行列索引值)

改進算法

求出字符串匹配矩陣是很簡單的事情,但是當我們得到0/1矩陣時,我們要怎麼去找到最長的對角線呢?這是一個比較麻煩的問題。

我們可以對這個算法進行改進,即在填充這個矩陣的值的時候,不僅僅是0和1,而是把公共子串的最大長度的值填入相應的位置。我們的目的很清楚,就是在圖1的基礎上把最長的對角線的值改爲1.2.3…,代表了公共子串的長度,看下面兩個圖會比較清楚:
在這裏插入圖片描述
在這裏插入圖片描述
我們想一下,把矩陣變成這樣之後,每個元素的值m[i][j]代表的是子串s1的前i部分和子串s2的前j部分中最大公共子串的長度。那麼其實這個矩陣中最大的元素的值就代表了公共子串的最大長度,而且通過這個最大元素的行列索引我們就可以定位到這個公共子串在原字符串中的結束位置,再通過長度就可以反推出公共子串的內容了。下面我們講一下代碼思路:

思路:當兩個字符串中找到匹配的字符之後,s1[i]=s2[j],那麼我們要填入矩陣[i,j]位置的值取決於它的左上方的值,填入的值爲左上方的值+1,代表這個字符可以加入現有的公共子串。

代碼

def find_lcsubstr(s1, s2):
    m = [[0 for i in range(len(s2) + 1)] for j in range(len(s1) + 1)]  # 生成0矩陣,爲方便後續計算,比字符串長度多了一列
    mmax = 0  # 最長匹配的長度
    p = 0  # 最長匹配對應在s1中的最後一位
    for i in range(len(s1)):
        for j in range(len(s2)):
            if s1[i] == s2[j]: # 如果相等,則加入現有的公共子串
                m[i + 1][j + 1] = m[i][j] + 1
                if m[i + 1][j + 1] > mmax:
                    mmax = m[i + 1][j + 1]
                    p = i + 1
    return s1[p - mmax:p], mmax  # 返回最長子串及其長度

問題描述2

求兩個字符串的最大公共子序列(LCS)(序列可以不連續)
子串要求字符必須是連續的,但是子序列就不是這樣。最長公共子序列是一個十分實用的問題,它可以描述兩段文字之間的“相似度”,即它們的雷同程度,從而能夠用來辨別抄襲。對一段文字進行修改之後,計算改動前後文字的最長公共子序列,將除此子序列外的部分提取出來,這種方法判斷修改的部分,往往十分準確。

例如:alibaba和ababila的最大公共子序列爲ababa

算法思路

思路:還是使用一個匹配矩陣來存放相關信息。此時矩陣的元素代表字符串s1前i個字符與字符串s2前j個字符的最大公共子序列長度。那麼如何填充這個矩陣呢?
對於字符串a和字符串b,長度分別爲m,n,先考慮他們的最後一個字符。

  1. 如果相等,說明這個字符一定可以成爲最大公共子序列的最後一個字符,那麼我們就可以不管這個字符,去求a[0:m-1] 和 b[0:n-1]的最大公共子序列。
    對於矩陣來說,這個關係就是:如果s1[i] = s2[j],那麼m[i][j] = m[i-1][j-1] + 1
    圖中就表示爲斜對角左上方的值加1
  2. 如果不等,那麼就變成去求a[0:m-1]和b[0:n](丟棄a的最後一位) 或者a[0:m]和 b[0:n-1](丟棄b的最後一位)的最大公共子序列
    對於矩陣來說,關係是:m[i][j] = MAX{m[i-1][j], m[i][j-1]}
    圖中就表示爲是上方的值和左方的值中取大的那個

例如:圖2中紅色的2:判斷行列字符都是b,因此爲左上角1+1得到2;圖2中綠色的3:判斷行列字符b和a不等,取上方和左方的最大值3.
在這裏插入圖片描述
整個矩陣求出之後,右下角的元素值就是字符串a和b的最大公共子序列的長度了。

根據上文的推到思路可以找到回溯思路,從右下角開始
(1)如果當前元素等於其左上角加1,那麼當前元素行列相等,是公共子序列中的一個,保存下來。
2)如果當前元素來自上方或左方,當前元素不保存。

按照此思路,從5開始,形成如圖所示的回溯路線,只有斜向上的箭頭對應的行元素納入最大公共子序列,求得最大公共子序列爲ababa。
在這裏插入圖片描述

代碼

一個矩陣記錄兩個字符串中匹配情況,若是匹配則爲左上方的值加1,否則爲左方和上方的最大值。一個矩陣記錄轉移方向,然後根據轉移方向,回溯找到最長子序列。

def find_lcseque(s1, s2):
    # 生成字符串長度加1的0矩陣,m用來保存對應位置匹配的結果
    m = [[0 for x in range(len(s2) + 1)] for y in range(len(s1) + 1)]
    # d用來記錄轉移方向
    d = [[None 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
                d[p1 + 1][p2 + 1] = 'ok'
            elif m[p1 + 1][p2] > m[p1][p2 + 1]:  # 左值大於上值,則該位置的值爲左值,並標記回溯時的方向
                m[p1 + 1][p2 + 1] = m[p1 + 1][p2]
                d[p1 + 1][p2 + 1] = 'left'
            else:  # 上值大於左值,則該位置的值爲上值,並標記方向up
                m[p1 + 1][p2 + 1] = m[p1][p2 + 1]
                d[p1 + 1][p2 + 1] = 'up'
    (p1, p2) = (len(s1), len(s2))
    print(numpy.array(d))
    s = []
    while m[p1][p2]:  # 不爲None時
        c = d[p1][p2]
        if c == 'ok':  # 匹配成功,插入該字符,並向左上角找下一個
            s.append(s1[p1 - 1])
            p1 -= 1
            p2 -= 1
        if c == 'left':  # 根據標記,向左找下一個
            p2 -= 1
        if c == 'up':  # 根據標記,向上找下一個
            p1 -= 1
    s.reverse()
    return ''.join(s)

if __name__ == '__main__':
    print(find_lcseque( 'alibaba','ababila'))

對這個回溯矩陣比劃一下可以發現跟上面畫的圖一模一樣
在這裏插入圖片描述
最後附上參考的博客並表示感謝:
https://blog.csdn.net/wateryouyo/article/details/50917812
https://blog.csdn.net/yebanxin/article/details/52190683
https://blog.csdn.net/yebanxin/article/details/52186706

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章