第44天,动态规划part11,子序列题型part02(ง •_•)ง💪,编程语言:C++
目录
1143.最长公共子序列
1035.不相交的线
53.最大子序和
392.判断子序列
总结
1143.最长公共子序列
文档讲解:代码随想录最长公共子序列
视频讲解:手撕最长公共子序列
题目:1143. 最长公共子序列 - 力扣(LeetCode)
学习:本题与最大重复子数组的不同在于,本题的序列不要求连续。类似于“最长上升子序列”和“最长连续递增序列”的区别。因此本题最大的不同就在于dp数组的设置以及递推公式。
从动归五部曲出发:
1.确定dp数组以及下标的含义:由于本题是子序列,不要求元素之间是连续的。因此本题可设置一个二维dp数组,dp[i][j]表示长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]。至于为什么是i - 1和 j - 1。其实是和上题一样,简化初始化步骤,否则需要对第一行和第一列单独进行初始化。
2.确定递推公式:主要有两个判断情况: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]不相同,那就看看dp[i - 1][j] 和 dp[i][j - 1]的情况,取最大值,这个地方其实是包含了两个字符串能否删减的思想,因为一个是比较text1[0, i - 2]与text2[0, j - 1],另一个是比较text1[0, i - 1]与text2[0, j - 2],相当于是每个字符串都考虑少一个元素的情况。这个思想在后面的题目中也有所体现。
3.dp数组初始化:由于我们设置的是i - 1和 j - 1,因此第一行和第二行设置为0即可,其余的情况会自行得到。
4.确定遍历顺序:由递推公式明显可以,有三个方向可以推出dp[i][j]。因此遍历顺序可以为从上到下,从左到右。
5.举例推导dp数组:
代码:
//时间复杂度O(n*m)
//空间复杂度O(n*m)
class Solution {
public:int longestCommonSubsequence(string text1, string text2) {if(text1.size() == 0 || text2.size() == 0) return 0;//动态规划 //1.确定dp数组以及下标的含义://dp[i][j]长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]vector<vector<int>> dp(text1.size() + 1, vector<int> (text2.size() + 1, 0));//2.确定递推公式//判断text1[i - 1] 和 text2[j - 1]的两种可能//3.初始化dp数组:第一行和第一列为0//4.确定遍历顺序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()];}
};
1035.不相交的线
文档讲解:代码随想录不相交的线
视频讲解:手撕不相交的线
题目:1035. 不相交的线 - 力扣(LeetCode)
学习:本题实际上是上一题的一个应用,由于只有相等的数能够连线,且不能够出现相交的线。那么就可以认为是找到最长的公共子序列,且子序列之间顺序不能调换。这样和上一题就是一摸一样的了,因此本题的解法也和上一题如出一辙。求解的最大连线数,也就是求解最长的公共子序列和。
因此本题的代码和上一题相同:
代码:
//时间复杂度O(n*m)
//空间复杂度O(n*m)
class Solution {
public:int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {//动态规划:1.确定dp数组以及下标的含义//dp[i][j]:表示nums1数组下标[0-(i - 1)]和nums2数组下标[0-(j-1)]内最大的公共子序列vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));//2.确定递推公式:同样试分析nums1[i - 1]和nums2[j - 1]的两种可能//3.初始化dp数组:由于我们使用了i-1和j-1下标,第一行和第一列初始化为0即可//4.确定遍历顺序int result = 0; //记录答案for(int i = 1; i <= nums1.size(); i++) {for(int j = 1; j <= nums2.size(); j++) {if(nums1[i - 1] == nums2[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(result < dp[i][j]) {result = dp[i][j];}}}return result;}
};
53.最大子序和
文档讲解:代码随想录最大子序和
视频讲解:手撕最大子序和
题目:53. 最大子数组和 - 力扣(LeetCode)
学习:本题有两种解题方法。第一种是使用贪心算法,我们只需要保证每次进行加减的是正数即可,因为正数才会对后续的加减产生有利的影响,这在我们贪心算法章节写写过:
代码:贪心算法
//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:int maxSubArray(vector<int>& nums) {int count = 0; //记录当前和int maxcount = INT_MIN; //记录最大值for(int i = 0; i < nums.size(); i++) {//如果当前和小于0,抛弃前和,改为当前值if(count < 0) {count = nums[i];}else {count += nums[i];}//如果当前和大于最大值,更新最大值if (count > maxcount) {maxcount = count;}}return maxcount;}
};
第二种方法则是采用动态规划的方式,从动归五部曲出发进行分析。
1.确定dp数组以及下标的含义:首先我们可以设置一个以为dp数组,dp[i]我们肯定是想要让它能够表示[0-i]区间内的最大和,这样dp[nums.size() - 1]就是要我们求解的值。但本题却不可以这么设置,因为本题要求的是子数组,是连续的元素。也就意味着实际上本题和“最大重复子数组”是一样的。我们确定nums[i]的时候,就已经对区间有限制了,必须要包含nums[i]。因此本题dp[i]表示包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。
2.确定递推公式:根据设置的含义,可以推出,dp[i]只能从两个方向推出:dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和;nums[i],即:从头开始计算当前连续子序列和。这两种可能,因为我们是不能够跨元素来取值的,因此只能借前面的值或者就只要当前的值。
3.初始化dp数组:显然dp[0] = nums[0],表示第一个元素的值。
4.确定遍历顺序:递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。
5.举例推导dp数组:
代码:
//时间复杂度O(n)
//空间复杂度O(n)
class Solution {
public:int maxSubArray(vector<int>& nums) {if(nums.size() == 1) return nums[0];//动态规划//1.确定dp数组以及下标的含义vector<int> dp(nums.size(), 0); //dp[i]表示以下标i结尾的连续子数组的最大和//2.确定递推公式//dp[i] = max(nums[i], dp[i - 1] + nums[i]);//3.初始化dp数组dp[0] = nums[0];int result = dp[0]; //保存答案//4.确定遍历顺序for(int i = 1; i < nums.size(); i++) {dp[i] = max(nums[i], dp[i - 1] + nums[i]);if(result < dp[i]) {result = dp[i];}}return result;}
392.判断子序列
文档讲解:代码随想录判断子序列
视频讲解:手撕判断子序列
题目: 392. 判断子序列 - 力扣(LeetCode)
学习:本题又是找子序列,稍微不同的是,本题需要我们判断s是否为t的子序列。其实也可以理解为t和s的最长公共子序列就是s,换句话说,只要t和s的最长公共子序列的长度是s.size(),那么就说明s是t的子序列,因为最长就只能达到s.size()这么长了。
由此本题的解法和最长公共子序列是一样的,只有一处可以进行部分修改,也就是递推公式,由于本题是判断是否是子序列,不需要我们真的找到最长子序列的长度,因此本题可以认为字符串s的各个字符是必须要包含的,因此当s[i - 1] 和 t[i - 1]不等时,可直接dp[i][j] = dp[i][j - 1],不用管s[i-1]的情况,因为字符串s是不能够减少的。(当然不改也不会有错误)
代码:
//时间复杂度O(n*m)
//空间复杂度O(n*m)
class Solution {
public:bool isSubsequence(string s, string t) {//1.确定dp数组以及下标的含义vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));for(int i = 1; i <= s.size(); i++) {for(int j = 1; j <= t.size(); 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]; //不同点,因为s字符串每个字符都必须考虑}}}return dp[s.size()][t.size()] == s.size();}
};
本题还有时间复杂度更低的解法,直观的一种就是可以采用双指针的解法,因为本题只需要判断一个字符串在另一个字符串中是否出现。
//时间复杂度O(n + m)
//空间复杂度O(1)
class Solution {
public:bool isSubsequence(string s, string t) {int n = s.length(), m = t.length();int i = 0, j = 0;while (i < n && j < m) {if (s[i] == t[j]) {i++;}j++;}return i == n;}
};
总结
子序列题型,需要十分注意dp数组的设置,是以i - 1为结尾的数组,还是[0-i-1]区间内的数组。取决于是否最后一个元素加入是有前置要求的。
假如最后一个元素是可以直接加入的,例如最大公共子序列,不相交的线等,那么就可以是[0-i- 1]的区间。
但加入最后一个元素加入的话,必须它前面没有元素,或者必须有它前面相邻的元素, 那么就不能是区间了,而需要时以i - 1为结尾。
还需要多加练习,记忆。