leetcode刷题笔记-1143.最长公共子序列

题目

https://leetcode.cn/problems/longest-common-subsequence/description/

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示

1
2
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

思路

这道题和718. 最长重复子数组的区别在于,718要求的是连续的子序列,而本题不要求子序列连续,就不是让你算子数组了。

虽然题目给出的是字符串,但本质上我们可以把它当作数组来处理,没有区别。

  • dp[i][j]代表字符串a中i-1和字符串b中j-1下标之前的最长公共子序列的长度。

这样做就可以不对数组提前进行初始化了,因为dp[0][x]dp[x][0]都是没有意义的。因为不会存在以下标-1为结尾的字符串,也自然没有公共子序列。换句话说,长度为0的字符串是不会有公共子序列的。

依照dp数组,可以想出递推的公式,当a[i-1]b[j-1]相同的时候,就说明子序列可以被扩展,dp[i][j] == dp[i-1][j-1]+1

如果不相同,则代表子序列断了,但注意我们dp数组的含义,它代表的是i-1和j-1之前的最长公共子序列的长度,即便i-1和j-1不相等,这个最长公共子序列依旧是有一个取值的,即选用“前一位”的公共子序列长度最大值。

但由于dp是一个二维数组,这里的“前一位”就有两个情况了,分别是dp[i-1][j]dp[i][j-1],对应含义是字符串a中前一位(根据dp数组,下标是i-1,公共子序列的含义是i-2)和b[j-1]能构成的最长公共子序列,以及字符串b中前一位和a[i-1]能构成的最长公共子序列的长度。需要用max来选取二者的最大值

递推的代码如下。

1
2
3
4
5
if (a[i - 1] == b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}

根据递推公式,dp[i][j]的值可以从dp数组中的三个方向推导出来,如下图所示。

image.png

剩下的代码就很简单了,因为我们不需要对dp数组进行提前初始化,直接用vector构造函数统一初始化为0就可以了,然后从1开始遍历直到字符串末尾,维护一个最大值,返回即可。

其实不维护最大值也是可以的,因为根据dp数组的定义,dp[a.size()][b.size()]就是我们需要的最大值。

代码1

完整的代码如下,具体参考注释中的说明。

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
class Solution {
public:
int longestCommonSubsequence(string a, string b) {
int result = 0; // 结果集初始化
// dp[i][j] 代表a中i和b中j下标和下标之前的最长公共子序列的长度
// i为0和j为0的情况下,长度为0的字符串是不会有公共子序列的,所以可以全部初始化为0
vector<vector<int>> dp(a.size() + 1, vector<int>(b.size() + 1, 0));
// 判断 a[i-1] == b[j-1] 代表公共子序列可以扩展
// dp[i][j] = dp[i-1][j-1] + 1
// 其他情况,代表公共子序列不能扩展,那么当前的最长公共子序列和前一位的相同
// 因为是二维数组,所以“前一位”其实有两种情况,分别是i-1和j-1,用max取最大值
for (int i = 1; i <= a.size(); i++) {
for (int j = 1; j <= b.size(); j++) {
if (a[i - 1] == b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
if (dp[i][j] > result) {
result = dp[i][j];
}
}
}
return result;
}
};

代码2:提前初始化

依照718题的思路,我们也可以写出一个提前初始化dp数组的代码,完整代码如下。

注意初始化的情况也有所不同,因为本题要求的是子序列,可以出现不连续的情况,即便a[i] != b[0],我们也需要沿用之前dp[i - 1][0]的结果,来保证初始化是正确的。

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
class Solution {
public:
int longestCommonSubsequence(string a, string b) {
int result = 0; // 结果集初始化
// dp[i][j] 代表a中i和b中j下标和下标之前的最长公共子序列的长度
vector<vector<int>> dp(a.size(), vector<int>(b.size(), 0));
// 初始化,判断第一个字符是否有相同的位置
for (int i = 0; i < a.size(); i++) {
if (a[i] == b[0]) {
dp[i][0] = 1;
result = 1;
} else if (i > 0) {
dp[i][0] = dp[i - 1][0];
}
}
for (int j = 0; j < b.size(); j++) {
if (b[j] == a[0]) {
dp[0][j] = 1;
result = 1;
} else if (j > 0) {
dp[0][j] = dp[0][j - 1];
}
}
// 判断 a[i] == b[j] 代表公共子序列可以扩展
// dp[i][j] = dp[i-1][j-1] + 1
// 其他情况,代表公共子序列不能扩展,那么当前的最长公共子序列和前一位的相同
// 因为是二维数组,所以“前一位”其实有两种情况,分别是i-1和j-1,用max取最大值
for (int i = 1; i < a.size(); i++) {
for (int j = 1; j < b.size(); j++) {
if (a[i] == b[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
if (dp[i][j] > result) {
result = dp[i][j];
}
}
}
return result;
}
};

image.png