题目:
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
前言:
假设字符串test1和字符串test1的长度分别为m和n,创建m+1行n+1列的二维数组dp,其中dp[i][j]表示text1[0:i]和text2[0:j]的最长公共子序列的长度。上述表示中,text1[0:i]表示text1的长度为i的前缀,text2[0:j]表示text2的长度为j的前缀。
考虑动态规划的边界情况:当i=0时,text1[0:i]为空,空字符串和任何字符串的最长公共子序列的长度都是0,因此对任意0≤j≤n,有dp[0][j]=0;同理当j=0时,dp[i][0]=0。因此动态规划的边界情况是:当i=0或j=0时,dp[i][j]=0。
当i>0且j>0时,考虑dp[i][j]的计算:
1、当text1[i-1]=text2[j-1]时,将这两个相同的字符称为公共字符,考虑text1[0:i-1]和text2[0:j-1]的最长公共子序列,再增加一个字符(即公共字符)即可得到text1[0:i]和text2[0:j]的最长公共子序列,因此dp[i][j]=dp[i-1][j-1]+1。
2、当text1[i-1]≠text2[j-1]时,考虑以下两项:text1[0:i-1]和text2[0:j]的最长公共子序列;text1[0:i]和text2[0:j-1]的最长公共子序列。要得到text1[0:i]和text2[0:j]的最长公共子序列,应取两项中的长度较大的一项,因此dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
由此可以得到状态转移方程:
最终计算得到dp[m][n]即为text1和text2的最长公共子序列的长度。
解法一(二维动态规划-迭代I):
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
//计算text1和text2的长度
int m = text1.length(), n = text2.length();
//定义两个text的最长公共子序列矩阵dp[i][j]。
//其中i是text1第i个字符结尾,j是text2第j个字符结尾。初始值dp[i][j]均为0。
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
//依次循环遍历text1和text2第i位和第j位的值,逐渐递增更新dp[i][j]的值。
for (int i = 1; i <= m; i++) {
char c1 = text1.at(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = text2.at(j - 1);
//当c1==c2时,根据状态转移方程,进行赋值更新
if (c1 == c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
//当c1≠c2时,根据状态转移方程,进行赋值更新
else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
};
时间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1和 text 2的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。空间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1和 text 2的长度。创建了 m+1 行 n+1 列的二维数组 dp。
解法二(二维动态规划-递归I):
通过递归思想实现自顶向下的遍历,如下为实现代码:
class Solution {
public:
int longestCommonSubsequence(string s, string t) {
int n = s.length(), m = t.length();
//定义以text1中i结尾和text2中j结尾的最长公共子序列关系矩阵
vector memo(n, vector<int>(m, -1)); // -1 表示没有计算过
auto dfs = [&](this auto&& dfs, int i, int j) -> int {
if (i < 0 || j < 0) {
return 0;
}
//防止重复计算导致时间复杂度升高,因此需要对res是否为-1进行判断
int& res = memo[i][j]; // 注意这里是引用
if (res != -1) {
return res; // 之前计算过,则不再需要再次进行计算了
}
if (s[i] == t[j]) {
return res = dfs(i - 1, j - 1) + 1;
}
return res = max(dfs(i - 1, j), dfs(i, j - 1));
};
return dfs(n - 1, m - 1);
}
};
时间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1和 text 2的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。空间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1和 text 2的长度。创建了 m+1 行 n+1 列的二维数组 dp。
笔者小记:
1、动态规划方法的实现需要确定两个必要条件,一个是边界条件;另一个是状态转移方程。可以通过迭代或递归的方式实现。当满足if条件语句时,激活状态转移方程。
2、关于字符串根据索引值获取字符的操作:at()
与 operator[]
比较
-
operator[]
不进行越界检查。如果索引越界,会导致程序崩溃或未定义行为。 -
at()
会进行越界检查,如果访问的索引无效,它会抛出一个std::out_of_range
异常。