最长公共子序列

最长公共子序列(Longest Common Subsequence,LCS)是动态规划中的经典问题。顾名思义,即求两个序列最长的公共子序列(可以不连续)。让我们来探讨一下这个问题。

在本文中,我们规定用 s [ − 1 ] s_{[-1]} s[1] 表示序列 s s s 的最后一个元素,用 s [ : − 1 ] s_{[:-1]} s[:1] 表示 s s s 去掉最后一个元素后的子序列。 L C S ( s 1 , s 2 ) \mathrm{LCS}(s_1, s_2) LCS(s1,s2) 表示 s 1 s_1 s1 s 2 s_2 s2 的LCS的长度。

现在,假设我们有两个字符串: s 1 = "abdcbab" s_1 = \text{"abdcbab"} s1="abdcbab" s 2 = "bdcbabb" s_2 = \text{"bdcbabb"} s2="bdcbabb"。我们要如何求它们的LCS呢?

  1. 状态转移

    • 如果两个序列的最后一个元素相同,比如 s 1 = "abecbab" s_1 = \text{"abecbab"} s1="abecbab" s 2 = "bdcbabb" s_2 = \text{"bdcbabb"} s2="bdcbabb",那么它们的LCS长度就是 s 1 [ : − 1 ] s_1[:-1] s1[:1] s 2 [ : − 1 ] s_2[:-1] s2[:1] 的LCS长度加上1。这是因为, s 1 s_1 s1 s 2 s_2 s2 的任何公共子序列 S S S 去掉最后一个元素后,都是 s 1 [ : − 1 ] s_{1[:-1]} s1[:1] s 2 [ : − 1 ] s_{2[:-1]} s2[:1] 的公共子序列,所以 S S S 的长度不会大于 L C S ( s 1 [ : − 1 ] , s 2 [ : − 1 ] ) + 1 \mathrm{LCS}(s_{1[:-1]}, s_{2[:-1]}) + 1 LCS(s1[:1],s2[:1])+1
    • 如果最后一个元素不同,则LCS长度应为 max ⁡ ( L C S ( s 1 [ : − 1 ] , s 2 ) , L C S ( s 1 , s 2 [ : − 1 ] ) ) \max(\mathrm{LCS}(s_{1[:-1]}, s_2), \mathrm{LCS}(s_1, s_{2[:-1]})) max(LCS(s1[:1],s2),LCS(s1,s2[:1])),因为这时两个序列的最后一个元素不会同时出现在LCS中。
  2. 边界条件

    • s 1 s_1 s1 s 2 s_2 s2 中至少有一个为空时,LCS的长度为0。

综合一下,我们可以得到以下式子:

L C S ( s 1 , s 2 ) = { 0 , if  s 1  or  s 2  is empty L C S ( s 1 [ : − 1 ] , s 2 [ : − 1 ] ) + 1 , if  s 1 [ − 1 ] = s 2 [ − 1 ] max ⁡ ( L C S ( s 1 [ : − 1 ] , s 2 ) , L C S ( s 1 , s 2 [ : − 1 ] ) ) , otherwise \mathrm{LCS}(s_1, s_2) = \begin{cases} 0, & \text{if } s_1 \text{ or } s_2 \text{ is empty} \\ \mathrm{LCS}(s_{1[:-1]}, s_{2[:-1]}) + 1, & \text{if } s_{1[-1]} = s_{2[-1]} \\ \max(\mathrm{LCS}(s_{1[:-1]}, s_2), \mathrm{LCS}(s_1, s_{2[:-1]})), & \text{otherwise} \end{cases} LCS(s1,s2)= 0,LCS(s1[:1],s2[:1])+1,max(LCS(s1[:1],s2),LCS(s1,s2[:1])),if s1 or s2 is emptyif s1[1]=s2[1]otherwise

我们可以轻松地将其翻译成一个递归程序(我直接用Python写了):

def lcs(s1, s2):
    if s1 == "" or s2 == "":
        return 0
    elif s1[-1] == s2[-1]:
        return lcs(s1[:-1], s2[:-1]) + 1
    else:
        return max(lcs(s1, s2[:-1]), lcs(s1[:-1], s2))

然而,直接这样写会引起时间和空间复杂度的爆炸。我们可以使用记忆化的方法,并且传参时传索引而不是序列本身。

此外,我们还可以使用递推的方法,将时间复杂度和空间复杂度优化到 O ( n 2 ) O(n^2) O(n2)。这一算法的时间复杂度和空间复杂度均为 O ( n 2 ) O(n^2) O(n2),如果使用滚动数组,可以将空间复杂度优化到 O ( n ) O(n) O(n)

你可能感兴趣的:(算法,算法)