两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

本文简要介绍了最长公共子序列求长度、求子序列本身及变种问题,并贴上了我自己的解答。

最长公共子序列:求长度

解题思路:见注释中的状态规划。

动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 状态:dp[i][j] := text1[0,i] 和 text2[0,j] 最长公共子序列的长度
// 转移:dp[i][j] = dp[i - 1][j - 1] + 1, text1[i] == text2[j]
// Math.max(dp[i][j - 1], dp[i - 1][j])
// 初始:dp[i][j] = 0;
// 结果:dp[len1][len2]
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return dp[len1][len2];
}
}
  • 时间复杂度:O(len1 * len2)

  • 空间复杂度:O(len1 * len2)

最长公共子序列:求子字符串本身

  • 求出最长公共子序列的长度
  • 倒序求出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public String longestCommonSupersequence(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 最长公共子序列
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
// 最长的子序列长度就是 dp[len1][len2]
StringBuilder sb = new StringBuilder();
int p1 = len1;
int p2 = len2;

while (p1 > 0 && p2 > 0) {
// 注意!这里是比较两个字符是否相等
if (text1.charAt(p1 - 1) == text2.charAt(p2 - 1)) {
sb.append(text1.charAt(p1 - 1));
p1--;
p2--;
} else if (dp[p1][p2] == dp[p1 - 1][p2]) {
p1--;
} else {
p2--;
}
}
return sb.reverse().toString();
}
}
  • 时间复杂度:O(len1 * len2)

  • 空间复杂度:O(len1 * len2)

Vjudge 的例题LCS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 可以直接通过 Vjudge 的代码
import java.util.*;

public class Main {
public static void main(String[] args) {
// ["abcicba\nabdkscab"]
Scanner sc = new Scanner(System.in);
String text1 = sc.nextLine();
String text2 = sc.nextLine();
int len1 = text1.length();
int len2 = text2.length();
// 最长公共子序列
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
// 最长的子序列长度就是 dp[len1][len2]
StringBuilder sb = new StringBuilder();
int p1 = len1;
int p2 = len2;

while (p1 > 0 && p2 > 0) {
// 注意!这里是比较两个字符是否相等
if (text1.charAt(p1 - 1) == text2.charAt(p2 - 1)) {
sb.append(text1.charAt(p1 - 1));
p1--;
p2--;
} else if (dp[p1][p2] == dp[p1 - 1][p2]) {
p1--;
} else {
p2--;
}
}
System.out.println(sb.reverse().toString());
}
}

变种:求超序

给出两个字符串 str1 和 str2,返回同时以 str1 和 str2 作为子序列的最短字符串。

解题思路:

  • 求出最长公共子序列的长度
  • 倒序求出超序字符串
    • 比较两个字符是否相等:text1.charAt(p1 - 1) == text2.charAt(p2 - 1),相等则可以使用一个字符作为公共字符。
    • 确定状态转移的方向: dp[p1][p2] == dp[p1 - 1][p2],相等则可以往 text1 方向迁移,不相等则可以往 text2 方向迁移。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public String shortestCommonSupersequence(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 最长公共子序列
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
// 最长的子序列长度就是 dp[len1][len2],如何根据这个信息找到最短超序呢?
StringBuilder sb = new StringBuilder();
int p1 = len1;
int p2 = len2;

while (p1 > 0 || p2 > 0) {
if (p1 == 0) {
sb.append(text2.charAt(--p2));
}
else if (p2 == 0) {
sb.append(text1.charAt(--p1));
}
// 注意!这里是比较两个字符是否相等
else if (text1.charAt(p1 - 1) == text2.charAt(p2 - 1)) {
sb.append(text1.charAt(p1 - 1));
p1--;
p2--;
} else if (dp[p1][p2] == dp[p1 - 1][p2]) {
sb.append(text1.charAt(p1 - 1));
p1--;
} else {
sb.append(text2.charAt(p2 - 1));
p2--;
}
}
return sb.reverse().toString();
}
}
  • 时间复杂度:O(len1 * len2)

  • 空间复杂度:O(len1 * len2)

变种:输出所有的最长公共子序列

参考:【动态规划】输出所有的最长公共子序列

解题思路:

  • 把 while 循环变成一个递归函数 helper。
  • dp[p1][p2 - 1] == dp[p1 - 1][p2] 时,说明可以往两个方向拓展,递归 helper 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public void longestCommonSupersequenceAll(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 最长公共子序列
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
// 最长的子序列长度就是 dp[len1][len2]
String sb = "";
int p1 = len1;
int p2 = len2;
Set<String> set = new HashSet<>();
helper(text1, text2, dp, sb, p1, p2, set);
for (String str: set)
System.out.println(str);
}

private void helper(String text1, String text2, int[][] dp, String sb, int p1, int p2, Set<String> set) {
while (p1 > 0 && p2 > 0) {
// 注意!这里是比较两个字符是否相等
if (text1.charAt(p1 - 1) == text2.charAt(p2 - 1)) {
sb += (text1.charAt(p1 - 1));
p1--;
p2--;
} else {
if (dp[p1][p2 - 1] == dp[p1 - 1][p2]) {
helper(text1, text2, dp, sb, p1 - 1, p2, set);
helper(text1, text2, dp, sb, p1, p2 - 1, set);
return;
} else if (dp[p1][p2 - 1] > dp[p1 - 1][p2]) {
p2--;
} else {
p1--;
}
}
}
set.add((new StringBuilder(sb).reverse()).toString());
}

子串和子序列的区别在于,子串必须是连续的。求最长公共子串的长度和求最长公共子序列的长度的方法几乎一样,并且比最长公共子序列问题简单许多。

最长公共子串:长度或者子串本身

动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public String shortestCommonSubstring(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 状态:dp[i][j] := text1[0,i] 和 text2[0,j] 最长公共子串的长度
// 转移:dp[i][j] = dp[i - 1][j - 1] + 1, text1[i] == text2[j]
// 0
// 初始:dp[i][j] = 0;
// 结果:maxLen
int[][] dp = new int[len1 + 1][len2 + 1];
int maxLen = 0;
int maxI = -1;
for (int i = 1; i <= len1; ++i) {
for (int j = 1; j <= len2; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > maxLen) {
maxI = i;
maxLen = dp[i][j];
}
}
}
}
if (maxLen == 0) return "";
return text1.substring(maxI - maxLen, maxI);
}
  • 时间复杂度:O(len1 * len2)

滑动窗口

LC718. 最长重复子数组 官方题解。是对动态规划的优化。

将字符串 text1 与 text2 对齐。从 text1[0] 对齐 text2[len2 - 1] 开始,直到 text1[len1 - 1] 对齐 text2[0]。

  • 时间复杂度:O(len1 len2) min(len1, len2)

输出所有的最长公共字串

具体解法同上,使用一个 list 记录 maxI 即可。代码略。