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基礎之區間型最長迴文子串一樣,乾巴爹=-=