动态规划-最长公共子串和最长公共子序列

2,109 阅读5分钟

最长公共子序列(Longest Common Subsequence)

  1. 题目描述: 给定两个字符序列, 找到两个字符中存在的最长公共子序列(子序列与子串的的区别在于, 子序列可以由原序列中不连续的字符构成). 所有字符串都是由大写字母构成.

  2. 解题思路

    1. 一个具体的例子: s1 = "ABCDGH" , s2 = "AEDFHR", 求s1, s2的最长公共子序列. 结果是ADH, 最长公共子序列长度为3.
    2. 求解的过程:
      1. (情况1)从最后的字符开始观察, 发现H与R不相同, 那么它们不可能同时出现在最终的公共子序列中, 因为已经到了最后一位了, 那么最长公共子序列只能由 ("ABCDG", "AEDFHR") 和 ("ABCDGH", "AEDFH")两种情况中最长的一个构成;
      2. 对于("ABCDG", "AEDFHR"), 继续观察最后的字符, 情况与1相同, 按照1的过程继续;
      3. (情况2)对于("ABCDGH", "AEDFH"), 发现最后的字符相同, 那么该字符一定可以出现在其最长公共子序列中, 所以该对的最长公共子序列为: ("ABCDG", "AEDF") + "H";
      4. 对于("ABCDG", "AEDF")又满足情况1;
      5. ...
    3. 总结: 对于两个序列s1, s2, 通过观察两个序列的最后一个字符, 可以分为两种情况:
      1. 相同: 那么最终的结果一定包含这个相同字符, 那么只需要继续寻找两个序列去掉该字符后的最长公共子序列, 将结果加上该同样的字符即可;
      2. 不同: 那么公共子序列最多只能包含其中一个序列的最后一个字符(也可能都不包含), 那么只需要取最大值即可
    4. 形式化表述: 设dp[i][j]为字符串s1i结尾的子序列和字符串s2j结尾的子序列构成的公共子序列长度, 那么:
    dp[i][j] =\begin{cases}
dp[i-1][j-1] + 1 & s1[i]=s2[j] \\
max(dp[i][j-1], dp[i-1][j]) & s1[i]!=s2[j]
\end{cases}
    1. 根据递推公式画表
    s1/s2 [_] A B C D G H
    [_] 0 0 0 0 D 0 0
    A 0 1 1 1 1 1 1
    E 0 1 1 1 1 1 1
    D 0 1 1 1 2 2 2
    F 0 1 1 1 2 2 2
    H 0 1 1 1 2 2 3
    R 0 1 1 1 2 2 3
    1. 解释: 根据公式,我们是需要从前往后算, 每一个d[i][j]需要它左边, 上面, 或左上方的点(结合公式和上表), 因此我们首先需要算出第一行和第一列的值, 如果直接处理传入的两个字符串, 我们就需要先过一遍两个字符串: 计算s1中的第一个字符是否出现在s2中, 计算s2中的第一个字符是否出现在s1中. 为了处理上的方便, 增加一行一列, 全部置为0. (可以认为在两个字符串开头添加了一个相同的字符, 这样第一行第一列的值就一定是1, 但是由于添加了一个字符,会导致整体的公共子序列长度多了1, 可以选择在最终的结果上减1, 这里选择将第一行第一列直接置为0.

    2. 从表最后一行最后一列可以看出, 最长公共子序列的长度为3.

      // 实现上的一个技巧, 如果不增加一行一列, 我们必须要先算出来第一行第一列,
      // 根据实际的两个字符串, 如果增加一列空字符, 直接初始化为0即可
      int[][] dp = new int[s1.length + 1][s2.length + 1];
      for (int i = 0; i < dp[0].length; i++) {
          dp[0][i] = 0;
      }
      for (int i = 0; i < dp.length; i++) {
          dp[i][0] = 0;
      }
      
    3. 找出最长公共子序列: 从最后一个开始找, 看看最长的公共子序列来自哪里, 只要出现斜线, 说明这个字符被记录下来了.

最长公共子串(Longest Common Substring)

  1. 题目描述: 给定两个字符序列, 找到两个字符串中存在的最长公共子串(子序列与子串的的区别在于, 子序列可以由原序列中不连续的字符构成, 子串必须是序列中连续的一部分).
  2. 看上去和子序列差不多, 但是用子序列的做法是错的. 对 s1 = "ABCDGH" , s2 = "AEDFH". 按照最长公共子串的方式来做: 当观察到末尾H相同时, 向前寻找, (ABCDG,AEDF)的最长公共子序列为AD, 但是由于这个子序列跟H不是连在一起,因此无法构成子串. 所以上述做法不对.
  3. 正确的做法: 寻找最长公共子序列时, 只有当两个字符相同时,我们才会考虑向前找, 同时要求前面的两个字符也是相同, 最终结果才加1. 因此, 我们遍历两个子串中所有字符相同的位置s1[i], s2[j], 并且认为该相同字符是最终最长公共子串的结尾元素, 将该子串长度记为dp[i][j], 此时我们只关心dp[i-1][j-1], 如果s1[i-1] == s2[j-1], 则结果加1, 否则以s1[i], s2[j](这里的s1[i], s2[j] 是相同字符)结尾的的最长公共子串就是1(可以认为s1[i], s2[j]由于不同而无法构成一个以其结尾的子串, 因此dp[i-1][j-1] == 0).
  4. 递推公式
dp[i][j] =\begin{cases}
    dp[i-1][j-1] + 1 & s1[i]=s2[j]\\
    0 & s1[i]!=s2[j]
\end{cases}

注意: 虽然公式只是差了一点, 当s1[i], != s2[j]时, 不是去最大,而是 dp[i-1][j-1] == 0, 但是意义是不一样的.

  1. 其余的部分与最长公共子序列一样, 可以类比.