1. 求最长公共子序列的长度
对于输入两个字符串 X, Y, 最长公共子序列(Longest Common Subsequence)中子序列只需保持相对顺序,并不要求连续。
首先,这是一个经典的动态规划题, 记
是字符串1
从
0
到索引
和字符串2 Y从 0 到索引
的最长公共子串的长度。
因此,这里需要定义一个二维的数组,其中,考虑到一些边界条件,可以让 dp 数组的维度加 1, 具体如下:
def LCS(str1, str2):
#最长公共子序列的长度
if not str1 or not str2:
return 0
dp = [[0 for j in range(len(str2)+1)] for i in range(len(str1)+1)]
for i in range(len(str1)):
for j in range(len(str2)):
if str1[i] == str2[j]:
dp[i+1][j+1] = dp[i][j] + 1
else:
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
return dp[len(str1)][len(str2)]
时间复杂度为
, 空间复杂度是
.
其中, m , n 分别是字符串X, 字符串 Y 的长度。
问题:能否降低空间复杂度?
一般写出上面基本OK了,但是一些面试官会要求能否降低空间复杂度。
这是肯定的,因为我们迭代公式,只涉及了
和
, 因此,只需要定义两个一维数组就行了。
def LCS2(str1, str2):
# 降低空间复杂度
if not str1 or not str2:
return 0
dp_1 = [0 for i in range(len(str2)+1)]
dp_2 = [0 for j in range(len(str2)+1)]
tag = True
for i in range(len(str1)):
for j in range(len(str2)):
if str1[i] == str2[j]:
if tag:
dp_2[j+1] = dp_1[j] + 1
else:
dp_1[j+1] = dp_2[j] + 1
else:
if tag:
dp_2[j+1] = max(dp_1[j+1], dp_2[j])
else:
dp_1[j+1] = max(dp_2[j+1], dp_1[j])
tag = tag ^ True
if tag:
return dp_1[-1]
return dp_2[-1]
优雅表达的话,定义一个
的二组数组 dp,没必要分开了定义。
示例:
str1 = "bcbb"
str2 = "bdcaba"
print(LCS(str1, str2))
print(LCS2(str1, str2))
结果:
3
3
2. 输出对应的子序列
有的面试官不会问最大子序列长度,而是,需要输出该子序列,如果有多个符合要求,输出一个即可。
输出最长子序列, 比如"bcab" 和 "bdcba" 是, "bcb" 或者 "bca".
这里,很简单,以下图为例(来源知乎@Zopen):
因此,在 1 是获得dp二维数组之后,只需要从最大长度出发,找到最后那个元素,然后,在往前遍历。
def LCS(str1, str2):
#最长公共子序列的长度, 以及对应的子序列
if not str1 or not str2:
return 0, ""
dp = [[0 for j in range(len(str2)+1)] for i in range(len(str1)+1)]
for i in range(len(str1)):
for j in range(len(str2)):
if str1[i] == str2[j]:
dp[i+1][j+1] = dp[i][j] + 1
else:
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
# 输出公共子序列
common_str = ""
max_common_len = dp[len(str1)][len(str2)]
index_str1 = len(str1)
index_str2 = len(str2)
#while index_str1 > 0:
while index_str2 > 0:
if index_str1 <= 0:
break
# 是最长子序列
if dp[index_str1][index_str2] == max_common_len:
# 是公共元素
if str1[index_str1-1] == str2[index_str2-1]:
common_str = str1[index_str1-1] + common_str
max_common_len -= 1
index_str1 -= 1
index_str2 -= 1
else:
index_str2 -= 1
else:
# 如果在 index_str1这行没找到, 公共元素,说明索引index_str1对应的元素不是公共元素, index_str1减一。
index_str1 -= 1
index_str2 += 1
return dp[len(str1)][len(str2)], common_str
回溯过程的时间复杂度是, O(m+n),m, n 是子串1,2的长度。 首先, index_str2
减到0 是O(n)
, 但是, 有 index_str2 += 1
操作,总共操作了O(m)
次。
问题:如果需要把所有的最长公共子序列输出呢
这里,由于需要求所有的解,直接使用回溯算法即可。回溯的过程,如果,dp[i-1][j]
和 dp[i][j-1]
相等,而且相应的元素是公共元素,那边就有两个方向进行回溯;其他情况是只有一个方向进行回溯。 因此,回溯就能写出来了:
def LCS(str1, str2):
#最长公共子序列的长度
if not str1 or not str2:
return 0, ""
dp = [[0 for j in range(len(str2)+1)] for i in range(len(str1)+1)]
for i in range(len(str1)):
for j in range(len(str2)):
if str1[i] == str2[j]:
dp[i+1][j+1] = dp[i][j] + 1
else:
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
#return dp[len(str1)][len(str2)]
# 输出公共子序列
common_str = ""
max_common_len = dp[len(str1)][len(str2)]
index_str1 = len(str1)
index_str2 = len(str2)
while index_str2 > 0:
if index_str1 <= 0:
break
if dp[index_str1][index_str2] == max_common_len:
if str1[index_str1-1] == str2[index_str2-1]:
common_str = str1[index_str1-1] + common_str
max_common_len -= 1
index_str1 -= 1
index_str2 -= 1
else:
index_str2 -= 1
else:
index_str1 -= 1
index_str2 += 1
print("track",trackBack(str1, str2, dp, len(str1), len(str2), dp[len(str1)][len(str2)],
lcs_str="", all_lcs_str=[]))
return dp[len(str1)][len(str2)], common_str
测试样例:
str1 = "bcab"
str2 = "bdcba"
print(LCS(str1, str2))
dp 矩阵:
[[0 0 0 0 0 0]
[0 1 1 1 1 1]
[0 1 1 2 2 2]
[0 1 1 2 2 3]
[0 1 1 2 3 3]]
回溯结果:
track ['bca', 'bcb']
至此,最长公共子序列问题解答完成。