前言
通过前面有关动态规划经典问题如背包问题、打家劫舍系列问题和股票投资问题的学习相信小伙伴跟荔枝一样对于动态规划题目有了一定的感觉。接下来再这篇文章中荔枝会继续梳理有关动态规划的经典系列问题------子序列和子串问题,给出解题的分析思路和具体的题解,希望能帮助到有需要的小伙伴。
文章目录
[一、Leecode300. 最长递增子序列](#一、Leecode300. 最长递增子序列)
[1.1 题目分析](#1.1 题目分析)
[1.2 题解示例](#1.2 题解示例)
[2.1 题目分析](#2.1 题目分析)
[2.2 题解示例](#2.2 题解示例)
[3.1 题目分析](#3.1 题目分析)
[3.2 题解示例](#3.2 题解示例)
[4.1 题目分析](#4.1 题目分析)
[4.2 题解示例](#4.2 题解示例)
[5.1 题目分析](#5.1 题目分析)
[5.2 题解示例](#5.2 题解示例)
[6.1 题目分析](#6.1 题目分析)
[6.2 题解示例](#6.2 题解示例)
一、Leecode300. 最长递增子序列
题目描述:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入样例:
nums = [10,9,2,5,3,7,101,18]
输出样例:
4
1.1 题目分析
从题目中的描述中我们知晓要求的是最长的严格递增子序列长度,子序列可以不连续但相对位置不能发生改变。按照动态规划的解题顺序来分析一下这道题目:
dp数组的含义
dp[i]:i之前包括i的以nums[i]结尾的最长递增子序列的长度
dp数组推导式
下标为i的dp[i]代表着i之前包括i的以nums[i]结尾的最长递增子序列的长度,如果 j 在 i 的左侧且nums[i] > nums[j],那么dp[i]就取到 j 从0到 i-1 的所有递增子序列的最大值+1并更新dp[i]。简单来说其实就是对i的左侧进行遍历,如果出现nums[i] > nums[j]则代表着dp[i]的数组进行
cpp
for(int j = 0;j<i;j++){
if(nums[i]>nums[j]) dp[i] = max(dp[i],dp[j]+1);
}
初始化和遍历顺序
由于我们这里求的是递增子序列,因此我们需要依赖于前面的元素和当前的值作比较,所以这里采用正序遍历比较好。而不管遍历哪一个下标值,最小的递增子序列的长度就是1,所以就把dp数组中的所有元素都初始化为1。
1.2 题解示例
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()<=1) return nums.size();
vector<int> dp(nums.size(),1);
int result = 0;
for(int i=1;i<nums.size();i++){
for(int j = 0;j<i;j++){
if(nums[i]>nums[j]) dp[i] = max(dp[i],dp[j]+1);
}
if(dp[i] > result) result = dp[i];
}
return result;
}
};
// dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、Leecode674.最长连续递增子序列
题目描述:
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
输入样例:
nums = [1,3,5,4,7]
输出样例:
3
2.1 题目分析
这道题目相比于Leecode300.最长递增子序列而言多了一个要求,那就是要求的这个子序列是一个连续递增的子序列,那么我们的dp数组推导式就要随之发生变化,因为是连续递增的子序列,所以相对来说这道题目的难度应该是会更小一点。
dp数组的含义
dp[i]:i之前包括i的以nums[i]结尾的最长递增子序列的长度
dp数组推导式
因为是连续递增,所以我们不需要再遍历前面的所有递增子序列和dp[i]来比较大小了,而是仅需要再满足nums[i]>nums[i-1]这个要求之后取到dp[i-1]+1的大小并随之更新dp[i]来获得最大的递增子序列。
cpp
//需要注意的是这里的max()是为了获得最大值并更新dp[i]
if(nums[i]>nums[i-1]) dp[i] = max(dp[i],dp[i-1]+1);
初始化和遍历顺序
这里的初始化和遍历顺序的选择与求非连续递增子序列是一样的。
2.2 题解示例
cpp
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if(nums.size()==0) return 0;
vector<int> dp(nums.size(),1);
int result = 1;
for(int i=1;i<nums.size();i++){
if(nums[i]>nums[i-1]) dp[i] = max(dp[i],dp[i-1]+1);
result = max(result,dp[i]);
}
return result;
}
};
//dp[i]:以i结尾的最长连续递增的子序列的长度
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-continuous-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
三、Leecode718.最长重复子数组
题目描述:
给两个整数数组
nums1
和nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度。输入样例:
nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出样例:
3
3.1 题目分析
通过前面两个问题的学习,我们知晓了如何求一个数组中的最长递增序列,而这道题目是求解两个数组中的最长公共子数组,由于是数组,所以一定是连续的。首先我们依旧需要明确dp数组的含义,对于这道题目由于是比较两个数组,所以我们自然而然地就想出使用一个二维数组来表示两个数组中地元素遍历情况。
dp数组的含义
dp[i][j]:以下标i-1为结尾的A,和以下标j-1为结尾的B,最长重复子数组长度为dp[i][j]
dp数组推导式
确定好dp数组的含义后,我们知晓对于dp[i][j]来说,它代表着A数组取到下标为i-1和B数组取到下标为 j-1的时候取到的最长重复子数组的长度,如果nums1[i-1]和nums2[j-1]是相等的,那么就意味着dp[i][j] = A数组取到下标为i-2和B数组取到下标为 j-2的时候取到的最长重复子数组的长度+1
cpp
if(nums1[i-1]==nums2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}
初始化和遍历顺序
由于我们求解的是两个数组之间的最长公共子数组,其最小值为0,因此再初始化的时候可以将所有的dp数组都初始化为0。遍历顺序的选择我们需要根据dp数组的推导式来做出判断,我们知晓当前状态是有前一个状态来推导出来的,所以我们选择正序遍历并选择一个变量result来更新最大的公共子序列。
3.2 题解示例
cpp
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
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;
result = max(result,dp[i][j]);
}
}
return result;
}
};
//只要想到用二维数组可以记录两个字符串的所有比较情况,这样就比较好推递推公式了
//dp[i][j]:以下标i-1为结尾的A,和以下标j-1为结尾的B,最长重复子数组长度为dp[i][j]
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-continuous-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
四、Leecode1143.最长公共子序列
题目描述:
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入样例:
text1 = "abcde", text2 = "ace"
输出样例:
3
4.1 题目分析
注意题目的描述,这道题目其实要我们求的是最长重复子序列!
dp数组的含义
dp[i][j]:以下标i-1为结尾的字符串text1和以下标j-1为结尾的字符串B的最长重复子序列长度为dp[i][j]
dp数组推导式
确定好dp数组的含义后,我们知晓对于dp[i][j]来说,如果text1[i - 1] == text2[j - 1],那么我们只需要知晓i-2和j-2时候的最长重复子序列的长度再+1即可。如果不相等,由于题目要求的是相对位置保持不变,所以这时候需要判断两个数组分别回撤一个下标后取得的最长重复子序列的值即可。
cpp
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[0][j]和dp[i][0]来说,如果是空串最长的公共子序列就是0,因此我们把他们初始化为0即可。遍历的时候根据dp递推式是从前到后,从上到下的遍历顺序来执行遍历的,因此均设置为正序遍历即可。
4.2 题解示例
cpp
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()];
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-common-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
**易错点:**在遍历的时候一定一定要注意的是------dp数组的含义不能忘,为什么要从1开始遍历,为什么选到nums.size(),这些需要考虑。
五、Leecode1035.不相交的线
题目描述:
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:nums1[i] == nums2[j],且绘制的直线不与任何其他连线(非水平线)相交。请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。以这种方法绘制线条,并返回可以绘制的最大连线数。
输入示例:
nums1 = [1,4,2], nums2 = [1,2,4]
输出示例:
2
5.1 题目分析
这道题目的难点在于如何对题目求解的问题进行转换成求解最长子序列,做完前面的题目其实我们发现,我们在最长子序列求解是的遍历是不回退的,也就是一旦n1[i] == n2[j],只要保证两个数组的相对位置不变,那么相同元素连成的线也就一定不相交。剩下的过程其实跟上一题求解子序列是一样的,大家可以发现甚至代码只需要改个数组名字就行哈哈哈~
5.2 题解示例
cpp
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=1;i<=nums1.size();i++){
for(int j=1;j<=nums2.size();j++){
if(nums2[j-1]==nums1[i-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[nums1.size()][nums2.size()];
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/uncrossed-lines
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
六、Leecode53.最大子序和
题目描述:
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。输入样例:
nums = [-2,1,-3,4,-1,2,1,-5,4]
输出样例:
6
6.1 题目分析
明确dp数组含义
dp[i]:代表着到i的连续数组的最大和
dp数组推导式
这道题目跟背包问题很像,dp[i]的最大值是由两个状态来推导出来的:nums[i]加入当前连续子序列和以及从当前元素开始计算,所以dp数组的推导式可以表示为:
cpp
dp[i] = max(dp[i-1]+nums[i],nums[i]);
初始化和遍历顺序
从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础,而dp[0]的值应该是选择nums[0],同时我们还需要借助一个result来记录连续数组的最大值。
6.2 题解示例
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 0) return 0;
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
int result = nums[0];
for(int i=1;i<nums.size();i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
if(result<dp[i]) result = dp[i];
}
return result;
}
};
// 贪心解法
// class Solution {
// public:
// int maxSubArray(vector<int>& nums) {
// int result = INT32_MIN;
// int count = 0;
// for(int i=0;i<nums.size();i++){
// count+=nums[i];
// if(count>result){
// result = count;
// }
// if(count<=0){
// count = 0;
// }
// }
// return result;
// }
// };
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/maximum-subarray
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
总结
最后,荔枝总结一下子序列和子串问题的难点吧。第一个是有关dp数组的设置,相信有些小伙伴会疑问为什么要设置成dp[i][j]代表着两个数组取i-1,j-1的时候的最大公共子序列,这其实是为了节省单独初始化dp[0][j]和dp[i][0]的步骤,但是带来的问题就是再写代码的时候会很容易就弄乱了思路,这时候就需要谨记dp数组的含义咯~接着其实就是转化的问题,对于不相交的直线我们怎么理解成求最大公共子序列问题;最后一个就是记得区分子串和子序列,两者在连续性上是不同滴,大家可以体会一下下哈哈哈,一起加油吧~~~
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~