最长公共子序列(Longest Common Subsequence)
-
题目描述: 给定两个字符序列, 找到两个字符中存在的最长公共子序列(子序列与子串的的区别在于, 子序列可以由原序列中不连续的字符构成). 所有字符串都是由大写字母构成.
-
解题思路
- 一个具体的例子: s1 = "ABCDGH" , s2 = "AEDFHR", 求s1, s2的最长公共子序列. 结果是ADH, 最长公共子序列长度为3.
- 求解的过程:
- (情况1)从最后的字符开始观察, 发现H与R不相同, 那么它们不可能同时出现在最终的公共子序列中, 因为已经到了最后一位了, 那么最长公共子序列只能由 ("ABCDG", "AEDFHR") 和 ("ABCDGH", "AEDFH")两种情况中最长的一个构成;
- 对于("ABCDG", "AEDFHR"), 继续观察最后的字符, 情况与1相同, 按照1的过程继续;
- (情况2)对于("ABCDGH", "AEDFH"), 发现最后的字符相同, 那么该字符一定可以出现在其最长公共子序列中, 所以该对的最长公共子序列为: ("ABCDG", "AEDF") + "H";
- 对于("ABCDG", "AEDF")又满足情况1;
- ...
- 总结: 对于两个序列s1, s2, 通过观察两个序列的最后一个字符, 可以分为两种情况:
- 相同: 那么最终的结果一定包含这个相同字符, 那么只需要继续寻找两个序列去掉该字符后的最长公共子序列, 将结果加上该同样的字符即可;
- 不同: 那么公共子序列最多只能包含其中一个序列的最后一个字符(也可能都不包含), 那么只需要取最大值即可
- 形式化表述: 设
dp[i][j]为字符串s1以i结尾的子序列和字符串s2以j结尾的子序列构成的公共子序列长度, 那么:
- 根据递推公式画表
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 -
解释: 根据公式,我们是需要从前往后算, 每一个
d[i][j]需要它左边, 上面, 或左上方的点(结合公式和上表), 因此我们首先需要算出第一行和第一列的值, 如果直接处理传入的两个字符串, 我们就需要先过一遍两个字符串: 计算s1中的第一个字符是否出现在s2中, 计算s2中的第一个字符是否出现在s1中. 为了处理上的方便, 增加一行一列, 全部置为0. (可以认为在两个字符串开头添加了一个相同的字符, 这样第一行第一列的值就一定是1, 但是由于添加了一个字符,会导致整体的公共子序列长度多了1, 可以选择在最终的结果上减1, 这里选择将第一行第一列直接置为0. -
从表最后一行最后一列可以看出, 最长公共子序列的长度为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; } -
找出最长公共子序列: 从最后一个开始找, 看看最长的公共子序列来自哪里, 只要出现斜线, 说明这个字符被记录下来了.
最长公共子串(Longest Common Substring)
- 题目描述: 给定两个字符序列, 找到两个字符串中存在的最长公共子串(子序列与子串的的区别在于, 子序列可以由原序列中不连续的字符构成, 子串必须是序列中连续的一部分).
- 看上去和子序列差不多, 但是用子序列的做法是错的. 对 s1 = "ABCDGH" , s2 = "AEDFH". 按照最长公共子串的方式来做: 当观察到末尾H相同时, 向前寻找, (ABCDG,AEDF)的最长公共子序列为AD, 但是由于这个子序列跟H不是连在一起,因此无法构成子串. 所以上述做法不对.
- 正确的做法: 寻找最长公共子序列时, 只有当两个字符相同时,我们才会考虑向前找, 同时要求前面的两个字符也是相同, 最终结果才加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). - 递推公式
注意: 虽然公式只是差了一点, 当s1[i], != s2[j]时, 不是去最大,而是 dp[i-1][j-1] == 0, 但是意义是不一样的.
- 其余的部分与最长公共子序列一样, 可以类比.