提示:DDU,供自己复习使用。欢迎大家前来讨论~
文章目录
- 题目
- 题目一:1143.最长公共子序列
- 解题思路:
- 题目二: 1035.不相交的线
- 解题思路:
- 题目三:53. 最大子序和
- 解题思路
- 题目四:392.判断子序列
- 解题思路
- 1. **确定 dp 数组的含义**
- 2. **确定递推公式**
- 3. **初始化 dp 数组**
- 4. **确定遍历顺序**
- 5. **举例推导 dp 数组**
- 6. **代码实现**
- 总结
动态规划Part11
题目
题目一:1143.最长公共子序列
1143. 最长公共子序列
解题思路:
动规五部曲分析如下:
- 确定dp数组(dp table)以及下标的含义
在动态规划中定义dp[i][j]
时,选择将字符串的长度定义为[0, i - 1]
而非[0, i]
的原因主要是为了简化代码实现,特别是在初始化dp
数组的第一行和第一列时。这种定义方式可以使得初始化逻辑更加直观和简便。尽管也可以选择[0, i]
作为定义方式,但前者在实际操作中更为常见,因为它减少了边界条件处理的复杂性。
- 确定递推公式
主要就是两大情况:
- text1[i - 1] 与 text2[j - 1]相同
- text1[i - 1] 与 text2[j - 1]不相同
如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][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]);
}
- dp数组如何初始化
在最长公共子序列问题中,dp[i][0]
和 dp[0][j]
都初始化为0,因为非空字符串与空字符串的最长公共子序列长度为0。其他dp
值根据递推关系计算得到。
代码:
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
- 确定遍历顺序
从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:
那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
- 举例推导dp数组
以输入:text1 = “abcde”, text2 = “ace” 为例,dp状态如图:
最后红框dp[text1.size()][text2.size()]为最终结果
以上分析完毕,C++代码如下:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
for (int i = 1; i <= text1.size(); i++) {
for (int j = 1; j <= text2.size(); j++) {
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[text1.size()][text2.size()];
}
};
- 时间复杂度: O(n * m),其中 n 和 m 分别为 text1 和 text2 的长度
- 空间复杂度: O(n * m)
题目二: 1035.不相交的线
1035. 不相交的线
解题思路:
绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交!
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
拿示例一A = [1,4,2], B = [1,2,4]为例,相交情况如图:
其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)
这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!
本题代码如下:
class Solution {
public:
int maxUncrossedLines(vector<int>& A, vector<int>& B) {
vector<vector<int>> dp(A.size() + 1, vector<int>(B.size() + 1, 0));
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]);
}
}
}
return dp[A.size()][B.size()];
}
};
- 时间复杂度: O(n * m)
- 空间复杂度: O(n * m)
题目三:53. 最大子序和
53. 最大子数组和
解题思路
这次我们用动态规划的思路再来分析一次。
动规五部曲如下:
- 确定dp数组(dp table)以及下标的含义
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。
- 确定递推公式
dp[i]只有两个方向可以推出来:
- dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
- nums[i],即:从头开始计算当前连续子序列和
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
- dp数组如何初始化
从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。
dp[0]应该是多少呢?
根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]。
- 确定遍历顺序
递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。
- 举例推导dp数组
以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下:
注意最后的结果可不是dp[nums.size() - 1]! ,而是dp[6]。
以上动规五部曲分析完毕,完整代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 0) return 0;
vector<int> dp(nums.size());
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式
if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值
}
return result;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
题目四:392.判断子序列
392. 判断子序列
解题思路
这个问题是判断一个字符串 s
是否为另一个字符串 t
的子序列。以下是解决这个问题的动态规划方法的关键步骤:
1. 确定 dp 数组的含义
dp[i][j]
表示字符串s
的前i
个字符和字符串t
的前j
个字符的最长相同子序列的长度。
2. 确定递推公式
- 如果
s[i-1]
等于t[j-1]
,则dp[i][j] = dp[i-1][j-1] + 1
。 - 如果
s[i-1]
不等于t[j-1]
,则dp[i][j] = dp[i][j-1]
,相当于在t
中跳过字符t[j-1]
。
3. 初始化 dp 数组
dp[0][j]
初始化为 0,表示空字符串s
与任意前缀的t
的子序列长度为 0。dp[i][0]
同理,初始化为 0。
4. 确定遍历顺序
- 从左到右、从上到下遍历,因为
dp[i][j]
的值依赖于其左边和上边的值。
5. 举例推导 dp 数组
- 通过具体例子(如
s = "abc"
,t = "ahbgdc"
)来手动计算dp
数组,验证递推公式的正确性。
6. 代码实现
class Solution {
public:
bool isSubsequence(string s, string t) {
int m = s.size(), n = t.size();
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++) {
if (s[i - 1] == t[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = dp[i][j - 1];
}
}
return dp[m][n] == m;
}
};
- 时间复杂度:O(n × m),其中
n
和m
分别是字符串s
和t
的长度。 - 空间复杂度:O(n × m),用于存储 dp 数组。
这种方法确保了通过动态规划有效计算两个字符串的最长相同子序列的长度,从而判断 s
是否为 t
的子序列。
总结
- 只考虑了编辑距离中的删除操作,而没有涉及插入和替换,这降低了问题的复杂性。
- 动态规划应用:通过动态规划方法,使用二维数组
dp
来记录和计算子问题的解,其中dp[i][j]
表示考虑字符串s
的前i
个字符和字符串t
的前j
个字符时的最长相同子序列的长度。 - 状态转移:
- 匹配情况:如果
s[i - 1]
等于t[j - 1]
,则在之前的基础上增加1,即dp[i][j] = dp[i - 1][j - 1] + 1
。 - 不匹配情况:如果
s[i - 1]
不等于t[j - 1]
,则取忽略s
的当前字符或忽略t
的当前字符所得到的最大长度,即dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
。
- 匹配情况:如果