1. 程式人生 > >dp基礎之雙序列型最長公共子串

dp基礎之雙序列型最長公共子串

問題:給定兩個字串A,B,找到兩個字元互傳的公共最長子串的長度。
子串:在原字串上去去掉某些字元後形成的字串,不改變字元之間的順序
例:
A = ['j', 'i', 'u', 'z', 'h', 'a', 'n', 'g']
B = ['l', 'i', 'j', 'i', 'a', 'n', 'g']
輸出:5( ['j', 'i', 'a', 'n', 'g'])


問題分析:
確定狀態:
如果A的長度是m,B的長度時n,現在考慮最優策略產生的子串
考慮A[m-1]和B[n-1]是否在最優策略的子串L裡(長度為L,有L個對應的對子)
    case1:最長公共子串裡沒有A[m-1],則最優策略的子串必定在A的前m-1個字元A[0,...,m-2]和
          B的前n個字元B[0,...,n-1]裡產生。
    case2:最長公共子串裡沒有B[n-1],則最優策略的子串必定在A的前m個字元A[0,...,m-1]和
          B的前n-1個字元B[0,...,n-2]裡產生。
    case3:A[m-1]和B[n-1]都在L裡,則最優策略的子串 = A的前m-1個字元A[0,...,m-2]和B的前n-1個字元B[0,...,n-2]產
          生的子串+A[m-1]
子問題:原問題求A的前m個字元A[0,...,m-1]和B的前n個字元B[0,...,n-1]產生的最長公共子串。
        現在要求A的前m-1個字元A[0,...,m-2]和B的前n個字元B[0,...,n-1]產生的最長公共子串
                A的前m個字元A[0,...,m-1]和B的前n-1個字元B[0,...,n-2]產生的最長公共子串
                A的前m-1個字元A[0,...,m-2]和B的前n-1個字元B[0,...,n-2]產生的最長公共子串
問題規模縮小。
轉移方程;設f[i][j]表示A的前i個字元A[0,...,i-1]和B的前j個字元B[0,...,j-1]產生的最長公共子串的長度。


        f[i][j] = max{ f[i-1][j] , f[i][j-1] , f[i-1][j-1] + 1 | A[i-1] = B[j-1] }
                  max{   case1   ,  case2    , case3   }
初始情況:
    空串和任何串的最長公共子串長度都為0
    f[0][j] = 0(j = 0,...,n)
    f[i][0] = 0(j = 0,...,m)

計算順序:
    f[0][0],f[0][1],...,f[0][n]
    .
    .
    .

    f[m][0],f[m][1],...,f[m][n]
答案f[m][n]

時間複雜度O(MN),空間複雜度O(MN),空間複雜度可以優化到O(N)

程式碼及註釋如下:

def longest_com_sub(A,B):
    m , n = len(A) , len(B)
    #建立f[i][j],f[i][j]表示A的前i個字元A[0,...,i-1]和B的前j個字元B[0,...,j-1]產生的最長公共子串的長度
    f = [[0 for j in range(n+1)] for i in range(m+1)]
    for i in range(m+1):
        for j in range(n+1):
            #初始化f[0][j] = 0(j = 0,...,n),f[i][0] = 0(j = 0,...,m),
            #其實初始化就已經賦值好了,僅為了思路更清楚
            if i == 0 or j == 0:
                f[i][j] = 0
                continue
            #f[i][j] = max{ f[i-1][j] , f[i][j-1] , f[i-1][j-1] + 1 | A[i-1] = B[j-1] }
            f[i][j] = max(f[i-1][j],f[i][j-1])
            if A[i-1] == B[j-1]:
                f[i][j] = max(f[i][j],f[i-1][j-1]+1)
    return f[m][n]
A = ['j', 'i', 'u', 'z', 'h', 'a', 'n','g']
B = ['l', 'i', 'j', 'i', 'a', 'n','g']
print(longest_com_sub(A,B))
#答案:5

 

如果要求是打印出該最長公共子序列,何解?

有兩種方法

程式碼及註釋如下:

#把該字串找出來
def find1_longest_com_sub(A,B):
    m , n = len(A) , len(B)
    #建立f[i][j],f[i][j]表示A的前i個字元A[0,...,i-1]和B的前j個字元B[0,...,j-1]產生的最長公共子串的長度
    f = [[0 for j in range(n+1)] for i in range(m+1)]
    #pai[i][j]用來記錄f[i][j]選擇的是哪一個case
    pai = [[0 for j in range(n+1)] for i in range(m+1)]
    for i in range(m+1):
        for j in range(n+1):
            #初始化f[0][j] = 0(j = 0,...,n),f[i][0] = 0(j = 0,...,m),
            #其實初始化就已經賦值好了,僅為了思路更清楚
            if i == 0 or j == 0:
                f[i][j] = 0
                continue
            #f[i][j] = max{ f[i-1][j] , f[i][j-1] , f[i-1][j-1] + 1 | A[i-1] = B[j-1] }
            #               case1       case2            case3
            f[i][j] = max(f[i-1][j],f[i][j-1])
            if f[i][j] == f[i-1][j]:
                #case1
                pai[i][j] = 1
                
            if f[i][j] == f[i][j-1]:
                #case2
                pai[i][j] = 2
            
            if A[i-1] == B[j-1]:
                k = j
                f[i][j] = max(f[i][j],f[i-1][j-1]+1)
                if f[i][j] == f[i-1][j-1] +1:
                    #case3
                    pai[i][j] = 3
                    
    #還原成字串,是從f[m][n],..,0
    res = []
    i = m
    j = n
    while i > 0 and j > 0:
        if pai[i][j] == 1:
            i -= 1
        else:
            if pai[i][j] == 2:
                j -= 1
            else:
                res.append(A[i-1])
                i -= 1
                j -= 1
    #因為是從最後一個開始放到陣列res裡,所以從後逆序列印
    print(res[::-1])                 
    return f[m][n]
A = ['j', 'i', 'u', 'z', 'h', 'a', 'n','g']
B = ['l', 'i', 'j', 'i', 'a', 'n','g']
print(find1_longest_com_sub(A,B))
#輸出:['j', 'i', 'a', 'n', 'g']
#5

 

#把該字串找出來
def find2_longest_com_sub(A,B):
    m , n = len(A) , len(B)
    #建立f[i][j],f[i][j]表示A的前i個字元A[0,...,i-1]和B的前j個字元B[0,...,j-1]產生的最長公共子串的長度
    f = [[0 for j in range(n+1)] for i in range(m+1)]
    #為了讓j的初始迴圈成功
    k = 0
    #res是最長公共子序列的結果
    res = []
    for i in range(m+1):
        for j in range(k,n+1):
            #初始化f[0][j] = 0(j = 0,...,n),f[i][0] = 0(j = 0,...,m)
            #其實初始化就已經賦值好了,僅為了思路更清楚
            if i == 0 or j == 0:
                f[i][j] = 0
                continue
            #f[i][j] = max{ f[i-1][j] , f[i][j-1] , f[i-1][j-1] + 1 | A[i-1] = B[j-1] }
            #               case1       case2            case3
            f[i][j] = max(f[i-1][j],f[i][j-1])
            if A[i-1] == B[j-1]:
                k = j
                f[i][j] = max(f[i][j],f[i-1][j-1]+1)
                if f[i][j] == f[i-1][j-1] +1:
                    #將找到的最長公共子序列的字元放到res裡
                    res.append(B[j-1])
                    #當找到一個公共字元時,本次j在B[j-1],那下次j的迴圈應該在B[j]開始,而沒必要從0開始
                    #因此令k = j
                    k = j
    
    print(res)                 
    return f[m][n]
A = ['j', 'i', 'u', 'z', 'h', 'a', 'n','g']
B = ['l', 'i', 'j', 'i', 'a', 'n','g']
print(find2_longest_com_sub(A,B))
#輸出:['j', 'i', 'a', 'n', 'g']
#5

 

未完待續。。。優化空間的程式碼自己琢磨一下吧就會了,

關鍵點在於轉移方程:f[i][j] = max{ f[i-1][j] , f[i][j-1] , f[i-1][j-1] + 1 | A[i-1] = B[j-1] }

可以看出來,計算f[i][j]時,f[i][j]是由它上方的 f[i-1][j]、它左方的f[i][j-1]、以及它左上方的f[i-1][j-1]三個數計算得到的,並且計算完f[i][j]再計算它右方的f[i][j+1]時,f[i][j]左上方f[i-1][j-1]不再用到,因此直接用f[0][n]和f[1][n]表示old和new兩個陣列,每計算完一輪,old-->new,new-->old,和dp基礎之區間型最長迴文子串一樣,乾巴爹=-=