一、LeetCode1143.最长公共子序列
题目链接/文章讲解/视频讲解:https://programmercarl.com/1143.%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97.html
状态:已解决
1.思路
这道题跟上篇的718题很像,但是718题要求连续的,此题求的则是子序列。使用动规:
(1)确定dp数组以及下标含义:
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列长度为dp[i][j]。
其实这里我纠结了好久,是定义dp[i][j]为以text1[i-1]和text2[j-1]结尾的公共子序列还是上述的定义,我最开始选的是前者,但是这样单靠两层 for循环是做不出来的,因为推出dp[i][j]的方向不是连续的,0~i-1,0~j-1任何一个地方都可能推出dp[i][j],因此要选后者定义。
至于为什么dp[i][j]代表的是 0~i-1 和 0~j-1,可以参考718题718. 最长重复子数组-CSDN博客
(2)确定递推公式:
主要是两个情况:
① text1[i-1] 与 text2[j-1] 相等时,此时 dp[i][j] 就是 dp[i-1][j-1] 的最长公共子序列的长度+1(最长公共子序列延申1位)。
② text1[i-1] 与 text2[j-1] 不相等时,此时 dp[i][j] 就不再是前面的最长公共子序列的长度+1了,而是直接继承前面的最长公共子序列,而前面的公共子序列是text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列 中最长的。为什么不是直接继承 dp[i-1][j-1]呢?首先,我们是i, j不是同步移动的,因此可能会出现dp[i-1][j]或者dp[i][j-1]子序列更长的情况(text1[i-1]==text2[j-2]或text1[i-2]==text2[j-1]);其次,dp[i][j-1]和dp[i-1][j]已经包含了dp[i-1][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]); }
(3) 初始化dp数组:
先看看dp[i][0]应该是多少呢?test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;
同理dp[0][j]也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
(4)确定遍历顺序:
正如刚刚的分析,我们是从上方和左边推出来的,故便顺序当然是从前往后、从上往下,即i, j都从小到大遍历。
(5)举例推导 dp数组:
以输入:text1 = "abcde", text2 = "ace" 为例,dp状态如图:
最后红框dp[text1.size()][text2.size()]为最终结果
2.代码实现
这里为了方便我自己的遍历习惯,将递推公式改了一下,变成了dp[i+1][j+1] = dp[i][j]+1,或者dp[i+1][j+1] = max(dp[i][j+1],dp[i+1][j]); 也就是遍历时范围还是 0~text1.size()-1。相当于刚刚的分析所有数组都向后移动一位。
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=0;i<text1.size();i++){
for(int j=0;j<text2.size();j++){
if(text1[i] == text2[j]){
dp[i+1][j+1] = dp[i][j]+1;
}else{
dp[i+1][j+1] = max(dp[i][j+1],dp[i+1][j]);
}
}
}
return dp[text1.size()][text2.size()];
}
};
时间复杂度: O(n * m),其中 n 和 m 分别为 text1 和 text2 的长度
空间复杂度: O(n * m)
二、1035.不相交的线
题目链接/文章讲解/视频讲解:https://programmercarl.com/1035.%E4%B8%8D%E7%9B%B8%E4%BA%A4%E7%9A%84%E7%BA%BF.html
状态:已解决
1.思路
这道题跟上道题一模一样,只是需要我们去看出它其实是一道求两个数组公共最长子序列的问题。我们现在来转换条件:
-
nums1[i] == nums2[j]:公共
- 且绘制的直线不与任何其他连线(非水平线)相交:暗示子序列,因为不能乱序,只能保证与原序列相同的顺序。
求:返回可以绘制的最大连线数,也就是求最长公共子序列了。于是我们可以直接用上题的代码。
2.代码实现
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
for(int i=0;i<nums1.size();i++){
for(int j=0;j<nums2.size();j++){
if(nums1[i] == nums2[j]){
dp[i+1][j+1] = dp[i][j]+1;
}else{
dp[i+1][j+1] = max(dp[i][j+1],dp[i+1][j]);
}
}
}
return dp[nums1.size()][nums2.size()];
}
};
时间复杂度: O(n * m),其中 n 和 m 分别为 text1 和 text2 的长度
空间复杂度: O(n * m)
三、53. 最大子序和
状态:已解决
1.思路
这道题我们做过,但是当时用的是贪心做法,现在要使用动规。
(1)确定dp数组以及下标含义:
dp[i]:以nums[i]为结尾的最大连续子序列和为dp[i]。
(2)确定递推公式:
dp[i]只有两个方向可以推出来:
① dp[i-1] + nums[i],即将 nums[i] 加入当前连续子序列和中。
② nums[i]:从自身开始作为子序列开头计算和
二者取最大值,故 dp[i] = max(dp[i-1] + nums[i], nums[i])
(3)dp数组初始化:
dp[i] 依赖于dp[i-1]的状态,故要初始化dp[0],由于dp[0]没有前面的元素可以拼接,因此只能作为子序列的开头,故dp[0] = nums[0]。
(4) 确定遍历顺序:
dp[i] 依赖于dp[i-1]的状态,故要从小到大遍历
(5)举例推导dp数组:
以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下:
注意,由于最长连续子序列和可能不是以nums[nums.size(0-1]结尾,故要在遍历过程中一直维护最大值。
2.代码实现
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
int maxSum = dp[0];
for(int i=1;i<nums.size();i++){
dp[i] = max(dp[i-1] + nums[i],nums[i]);
maxSum = max(maxSum,dp[i]);
//cout<<dp[i]<<endl;
}
return maxSum;
}
};
时间复杂度:O(n)
空间复杂度:O(n)