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):




java实现最长公共子序列 java求最长公共子序列_java实现最长公共子序列


因此,在 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']


至此,最长公共子序列问题解答完成。