📝 题目描述

题目链接最长公共子序列

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

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

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
示例 1:

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

示例 2:

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

示例 3:

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

提示:

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

💡 解题思路

方法一:动态规划

最长公共子序列问题是典型的二维动态规划问题。

假设字符串 text1text_1​ 和 text2text_2​ 的长度分别为 mmnn,创建 m+1m+1n+1n+1 列的二维数组 dpdp,其中 dp[i][j]dp[i][j] 表示 text1[0:i]text_1​[0:i]text2[0:j]text_2​[0:j] 的最长公共子序列的长度。

上述表示中,text1[0:i]text_1​[0:i] 表示 text1text_1​ 的长度为 ii 的前缀,text2[0:j]text_2​[0:j] 表示 text2text_2​ 的长度为 jj 的前缀,不是指“text1[0]text_1[0]text1[i]text_1[i]”,也就是说 text1[0:0]text_1​[0:0] 是空字符串。

接下来考虑动态规划的边界情况:

  • i=0i=0 时,text1[0:i]text_1​[0:i] 为空,空字符串和任何字符串的最长公共子序列的长度都是 00,因此对任意 0jn0≤j≤n,有 dp[0][j]=0dp[0][j]=0
  • j=0j=0 时,text2[0:j]text_2​[0:j] 为空,同理可得,对任意 0im0≤i≤m,有 dp[i][0]=0dp[i][0]=0

因此动态规划的边界情况是:当 i=0i=0j=0j=0 时,dp[i][j]=0dp[i][j]=0

i>0i>0j>0j>0 时,考虑 dp[i][j]dp[i][j] 的计算:

  • text1[i1]=text2[j1]text_1​[i−1]=text_2​[j−1] 时,将这两个相同的字符称为公共字符,考虑 text1[0:i1]text_1​[0:i−1]text2[0:j1]text_2​[0:j−1] 的最长公共子序列,再增加一个字符(即公共字符)即可得到 text1[0:i]text_1​[0:i]text2[0:j]text_2​[0:j] 的最长公共子序列,因此 dp[i][j]=dp[i1][j1]+1dp[i][j]=dp[i−1][j−1]+1
  • text1[i1]text2[j1]text_1​[i−1] \neq text_2​[j−1] 时,考虑以下两项:
    • text1[0:i1]text_1​[0:i−1]text2[0:j2]text_2​[0:j−2] 的最长公共子序列;
    • text1[0:i2]text_1​[0:i−2]text2[0:j1]text_2​[0:j−1] 的最长公共子序列。
      要得到 text1[0:i]text_1​[0:i]text2[0:j]text_2​[0:j] 的最长公共子序列,应取两项中的长度较大的一项,因此 dp[i][j]=max(dp[i1][j],dp[i][j1])dp[i][j]=max(dp[i−1][j],dp[i][j−1])

由此可以得到如下状态转移方程:

dp[i][j]={dp[i][j]=dp[i1][j1]+1, ​text1[i1]=text2[j1]max(dp[i1][j],dp[i][j1]), text1[i1]text2[j1]dp[i][j] = \begin{cases} dp[i][j]=dp[i−1][j−1]+1, \ ​text1​[i−1]=text2​[j−1] \\ max(dp[i−1][j],dp[i][j−1]), \ text1​[i−1] \neq text2​[j−1]​ \end{cases}

最终计算得到 dp[m][n]dp[m][n] 即为 text1text_1​ 和 text2text_2​ 的最长公共子序列的长度。

🔧 代码实现

1、动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
// 多分配一行一列,自动初始化为0,处理了边界问题
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 注意:字符串的索引要减 1
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
};

📊 复杂度分析

1、动态规划

  • 时间复杂度O(mn)O(mn),其中 mmnn 分别是字符串 text1text_1​ 和 text2text_2​ 的长度。二维数组 dpdpm+1m+1 行和 n+1n+1 列,需要对 dpdp 中的每个元素进行计算。
  • 空间复杂度O(mn)O(mn),其中 mmnn 分别是字符串 text1text_1​ 和 text2text_2​ 的长度。创建了 m+1m+1n+1n+1 列的二维数组 dpdp

🎯 总结

  • 核心思想:记住可以把 dpdp 数组的长宽各增加 11,这样就可以避免对边界条件进行额外处理,有点类似于链表的哨兵节点。