最長公共子序列圖解、算法實現和復雜度分析


LCS和萊文斯坦距離的解決思路非常類似,都是利用動態規划的方式來解決。可以參見上一篇“萊文斯坦距離”,兩個概念對比着看理解為更深入!

 

LCS定義

同樣引用百科:

最長公共子序列LCS)是一個在一個序列集合中(通常為兩個序列)用來查找所有序列中最長子序列的問題。與查找最長公共子串的問題不同的地方是:子序列不需要在原序列中占用連續的位置。最長公共子序列問題是一個經典的計算機科學問題,也是數據比較程序,比如Diff工具,和生物信息學應用的基礎。它也被廣泛地應用在版本控制,比如Git用來調和文件之間的改變。

 

狀態轉移方程

如果產出的是最長公共子序列的長度,則方程為:

 注意:上面狀態轉移方程中,最后的1是一個指示函數,表示如果a字符串第i個字符和b字符串第j個字符相同則為1,否則取0;

 

如果最終需要產出最長公共子序列的具體內容,則可以在動態規划的每一步驟保存當前LCS字符串,則最后一步產出的就是兩個輸入字符串的LCS。對應的狀態轉移方程也要適當變一下:

1. min(i, j) == 0時,lcs的取值從0改為空字符串;

2. 最后一行指示函數的值從1改為ai字符的內容。如下所示:

 

計算過程圖解

假設要計算str_a = "abcdd" 和 str_b = "aacbd" 的最長公共子序列,則可以橫向從左向右遍歷(方便起見,單元格里記錄的是lcs的長度,放lcs字符串的話寫不下。。):

如圖所示,按照狀態轉移方程,除了第一行和第一列之外的每個單元格,都只依賴其左、上、左上三個單元格的內容,所以每一行的計算只需要緩存當前橫行和上一橫行的內容即可。

 

代碼實現 

python實現代碼如下: 

 1 #-*- encoding:utf-8 -*-
 2 import sys
 3 import pdb
 4 
 5 
 6 def lcs(str_a, str_b):
 7     """最長公共子序列
 8     attributes:
 9         str_a: 字符串a
10         str_b: 字符串b
11     return:
12         兩個字符串的最長公共子序列內容
13     exception:
14         TypeError
15     """
16 
17     # 異常檢測
18     if not isinstance(str_a, basestring) or not isinstance(str_b, basestring):
19         raise TypeError("Input must be string!")
20 
21     # 定義lcs記錄矩陣
22     matrix = [["" for j in range(len(str_b) + 1)] for i in range(len(str_a) + 1)]
23     
24     for i in range(1, len(str_a) + 1):
25         for j in range(1, len(str_b) + 1):
26             sub_a = matrix[i - 1][j] # 上方單元格
27             sub_b = matrix[i][j - 1] # 左側單元格
28             sub_a_b = matrix[i - 1][j - 1] \
29                 + (str_a[i - 1] if str_a[i - 1] == str_b[j - 1] else "") # 左上單元格
30 
31             # 記錄下最長的字符串
32             tmp_str = sub_a if len(sub_a) > len(sub_b) else sub_b
33             matrix[i][j] = tmp_str if len(tmp_str) > len(sub_a_b) else sub_a_b
34 
35     return matrix[-1][-1]
36 
37 
38 def main(str_a, str_b):
39     ret = lcs(str_a, str_b)
40     print("lcs=%s, lcs_length=%s" % (ret, len(ret)))
41 
42 
43 if __name__ == '__main__':
44     main(sys.argv[1], sys.argv[2])
lcs_dp.py

執行結果

[work@yq01-kg-saa-dev-general0.yq01.baidu.com longest_common_subsequence]$ python lcs_dp.py abcde acdebbbbbb
lcs=acde, lcs_length=4

空間復雜度優化后的代碼。優化點有兩個:

1. 只創建一個2行的記錄矩陣,節省空間;

2. 記錄矩陣的列選取相對短的字符串的長度

 1 #-*- encoding:utf-8 -*-
 2 import sys
 3 import pdb
 4 
 5 
 6 def lcs(str_a, str_b):
 7     """最長公共子序列
 8     attributes:
 9         str_a: 字符串a
10         str_b: 字符串b
11     return:
12         兩個字符串的最長公共子序列內容
13     exception:
14         TypeError: 輸入的不是字符串
15     """
16 
17     # 異常檢測
18     if not isinstance(str_a, basestring) or not isinstance(str_b, basestring):
19         raise TypeError("Input must be string!")
20 
21     # 讓str_b為更短的字符串,這樣空間復雜度能更小一些
22     if len(str_a) < len(str_b):
23         tmp = str_b
24         str_b = str_a
25         str_a = tmp
26 
27     # 定義一個2 * (len(str_b)+1)的記錄矩陣
28     matrix = [["" for j in range(len(str_b) + 1)] for i in range(2)]
29     curr_i = 1
30 
31     for i in range(1, len(str_a) + 1):
32         for j in range(1, len(str_b) + 1):
33             sub_a = matrix[1 - curr_i][j]
34             sub_b = matrix[curr_i][j - 1]
35             sub_a_b = matrix[1 - curr_i][j - 1] \
36                 + (str_a[i - 1] if str_a[i - 1] == str_b[j - 1] else "")
37 
38             # 記錄下最長的字符串
39             tmp_str = sub_a if len(sub_a) > len(sub_b) else sub_b
40             matrix[curr_i][j] = tmp_str if len(tmp_str) > len(sub_a_b) else sub_a_b
41         curr_i = 1 - curr_i
42 
43     return matrix[1 - curr_i][-1]
44 
45 
46 def main(str_a, str_b):
47     ret = lcs(str_a, str_b)
48     print("lcs=%s, lcs_length=%s" % (ret, len(ret)))
49 
50 
51 if __name__ == '__main__':
52     main(sys.argv[1], sys.argv[2])
lcs_dp_opt.py

執行結果

$ python lcs_dp_opt.py abcde acdeb
lcs=acde, lcs_length=4

 

復雜度分析

類比萊文斯坦距離的復雜度:

1.  LCS的時間復雜度是O(m * n)

2. LCS的空間復雜度也是O(m * n),但同樣也可以優化成O(2 * min(m, n)),即可以達到O(n)級別

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM