文章目录
- 300.最长递增子序列(注意初始化和结果取值)
- 思路
- DP数组含义
- 递推公式
- 初始化
- 最开始的写法
- debug测试:解答错误
- debug测试2:result初始值
- 修改完整版
- 总结
- 674.最长连续递增序列
- 思路1:滑动窗口
- 思路2:动态规划
- DP数组含义
- 递推公式
- 初始化
- 完整版
- 718.最长重复子数组
- 思路1:DP数组定义为下标i和下标j
- DP数组含义
- 递推公式
- 初始化
- 遍历顺序
- 最开始的写法
- debug测试:解答错误
- 思路1完整版
- 总结
- 思路2:(推荐)DP数组定义为i对应nums[i-1]
- DP数组含义
- 递推公式
- 初始化
- 思路2完整版
- 总结
- 子序列系列是DP重要系列,关键就是掌握状态的递推,
dp[i]
可以从哪些状态推出来!
300.最长递增子序列(注意初始化和结果取值)
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4
思路
本题属于子序列类的题目,首先要明确什么是子序列。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。也就是说元素并不需要连续。
DP数组含义
本题正确定义DP数组含义非常重要。
dp[i]
的含义是,包含下标i在内,也就是包含nums[i]
数字在内的递增子序列的长度。
递推公式
本题的递推核心在于找下标i
范围以内的,能够满足递增条件的数字,增加到下标i
的长度里面。
例如,从最开始的dp[1]开始分析,因为递增序列最小长度为1所以初始值为1。
if(nums[1]>nums[0]) dp[1]=dp[0]+1
再分析dp[2],对于下标2,应分析所有下标2以内的数字,能不能与nums[2]构成递增。
if(nums[2]>nums[0]) dp[2]=dp[0]+1;
if(nums[2]>nums[1]) dp[2]=dp[1]+1;
由上面的推导逻辑可知,每遍历一个i
内部的j
,dp[i]
都会进行长度的更新,我们最后的dp[i]需要选长度最大的!
因此得到最后的递推公式:
dp[i]=max(dp[i],dp[j]+1);
完整的递推逻辑如下:
//注意i和j的取值,是从初始状态推过来的
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
//如果满足递增,那么更新dp[i]
if(nums[i]>nums[j]){
dp[i]=max(dp[i],dp[j]+1)
}
}
}
初始化
dp[0]=1
,因为递增序列的最短长度就是1(参考用例)。
但是本题需要特别注意,需要所有的dp[i]都初始化为1,而不是0!因为dp[i]指的是每个以下标i为结束的递增子序列的长度,该长度最少是1!
举个例子,当dp[2]=dp[1]+1
的时候,如果在这之前nums[1]<nums[0],dp[1]没有得到累加,那么dp[1]此刻的数值应该是1!才能让dp[2]是正确的最大长度!
最开始的写法
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int>dp(nums.size(),0);
//初始化,dp[0]是一切递推的基础
dp[0]=1;
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
//如果i的末尾元素大于j的末尾元素,那么就在j长度基础上+1,最后取最大的dp[i]
if(nums[i]>nums[j])
dp[i]=max(dp[i],dp[j]+1);
}
}
return dp[nums.size()-1];
}
};
debug测试:解答错误
由最后一个case我们可以看出问题,就是dp[i]的数值,没有保持到最后。
因此,我们应该在dp数组中,取所有dp[i]的最大值,因为这里的dp[i]是针对每个以下标i结束的子序列,所以最后的最大值不一定在dp数组末尾!
并且最开始写法的初始化也有问题,以i为结尾的递增子序列,其长度最少为1!
debug测试2:result初始值
另外还需要注意,因为i从1开始所以res并没有和dp[0]比较,因此res初始值也应该是1(防止只有一个元素的时候,根本不比较直接输出)
修改完整版
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int>dp(nums.size(),1);
//初始化,注意所有的dp[i]初始值都应该是1
int result=1;
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
//如果i的末尾元素大于j的末尾元素,那么就在j长度基础上+1,最后取最大的dp[i]
if(nums[i]>nums[j])
dp[i]=max(dp[i],dp[j]+1);//dp[i]存放dp[i]的最大值
}
result=(result>dp[i])?result:dp[i];
}
return result;
}
};
- 时间复杂度: O(n^2)
- 空间复杂度: O(n)
总结
- 本题的最大值并不在dp数组末尾,因为每个数据都可能比前面的数字要小。最大长度可能是dp数组中间位置的dp[i],所以需要建个result存最大值。
- 初始化要注意,本题每个以i为结尾的递增子序列,长度最小是1而不是0!
674.最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 10^4
-10^9 <= nums[i] <= 10^9
思路1:滑动窗口
本题是求连续递增子序列的长度,此时第一反应是滑动窗口。
滑动窗口的算法专题里面整理过类似的题目:算法专题整理:滑动窗口_大磕学家ZYX的博客-CSDN博客
上面这道题目求的也是连续的最长子序列的长度,这两道题的做法类似但是有一定的区别。
本题的滑动窗口做法:
- 遇到不是递增的,直接更新左端点
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if(nums.size()==1) return 1;
//开始滑动
int res=1;
int start=0;//先从下标0位置开始找
for(int i=1;i<nums.size();i++){
//只要不是递增,直接修改左端点
if(nums[i]<=nums[i-1]){
start=i;
}
res=max(res,i-start+1);//res存放长度的最大值
}
return res;
}
};
2779.数组最大美丽值的滑动窗口做法:
- 排序之后找左右窗口端点值相减<=2k的窗口,并求窗口最大长度
class Solution {
public:
int maximumBeauty(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int res=1;//这道题子序列是找相等元素
for(int left=0,right=0;right<nums.size();right++){
//因为是找最大长度,所以是不符合<=条件了,才移动左端点
while((nums[right]-nums[left])>2*k){
left++;
}
res=max(res,right-left+1);//res存放长度的最大值
}
return res;
}
};
从这两个题目对比也可以看出来,滑动窗口指的是遍历右端点,按照条件移动左端点。
在最长连续递增子序列的问题中,需要找的是数组中最长的递增子序列。所以当发现当前元素不大于前一个元素时,窗口的左端点直接跳到新的位置求新的递增长度,不需要慢慢滑动。
在数组最大美丽值中,需要找的是数组中最长的由相等元素组成的子序列,且可以将数组中的元素替换为它加减k的结果。因此当发现窗口right-k大于窗口left+k时,需要将窗口的左边界进行滑动。
这两道题目的滑动窗口思想是类似的,都是通过移动窗口的左右边界来找到最长的满足某种条件的子序列。
思路2:动态规划
但是其实没必要用滑动窗口,因为本题求递增子序列,左端点不用滑,只要一直枚举右端点能不能扩,断开的话将左端点移到右端点的位置即可。
本题可以像上一题最长递增子序列一样用DP解决。
DP数组含义
dp[i]指的是包括下标i的nums[i]在内的递增子序列的最大长度。
递推公式
- 本题是求连续的子序列,因此只需要一层循环即可,不需要两层循环找不连续的递增!
for(int i=0;i<nums.size();i++){
if(nums[i]>nums[i-1]){
dp[i]=dp[i-1]+1;//这么做相当于断开的时候重新计数!
}
}
初始化
和上一题一样,每个数字自身就是一个子序列,因此所有dp[i]都应该初始化为1!
完整版
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
vector<int>dp(nums.size(),1);
if(nums.size()==1) return 1;
//记录长度的初始值
int res=1;
for(int i=1;i<nums.size();i++){
if(nums[i]>nums[i-1]){
dp[i]=dp[i-1]+1;
}
res=max(res,dp[i]);//res存放dp[i]的最大值
}
return res;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
718.最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100
思路1:DP数组定义为下标i和下标j
本题难点在于,比较两个数组,得到重复且连续的最长子数组,用一维的DP数组无法同时表示两个数组的状态。
我们需要用一个二维的矩阵来比较两个数组的所有状态。
DP数组含义
第一种思路,我们直接让i对应下标i,j对应下标j。
dp[i][j]
表示第一个数组在下标i
位置,第二个数组在下标j
位置,此时最长重复子数组的长度是dp[i][j]
递推公式
- 核心点是连续,所以从
dp[i-1][j-1]
的状态转移过来
for(int i=1;i<nums.size();i++){
for(int j=1;j<nums.size();j++){
if(nums1[i]==nums2[j]){
//从i-1,j-1转移过来,因为是重复连续子序列,所以i-1和j-1必须也要重复且连续
dp[i][j]=dp[i-1][j-1]+1;
}
}
}
初始化
初始化包括dp[i][0]
和dp[0][j]
的情况。
dp[i][0]
的含义是,两个数组下标分别为i和0的情况下,重复子序列的长度。因此需要把num1的数字和nums2下标为0的数字进行一一比对,如果相等就需要+1!
dp[0][j]
同理。
//定义DP数组,i对应nums1,j对应nums2
vector<int>(nums1.size(),vector<int>(nums2.size(),0));
for(int i=0;i<nums1.size();i++){
if(nums1[i]==nums2[0]) dp[i][0]=1;
}
for(int j=0;j<nums2.size();j++){
if(nums2[j]==nums1[0]) dp[0][j]=1;
}
遍历顺序
遍历顺序任意一个for循环在外都可以
最开始的写法
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>>dp(nums1.size(),vector<int>(nums2.size(),0));
//初始化
for(int i=0;i<nums1.size();i++){
if(nums1[i]==nums2[0]) dp[i][0]=1;
}
for(int j=0;j<nums2.size();j++){
if(nums2[j]==nums1[0]) dp[0][j]=1;
}
//开始遍历
int result=0;
for(int i=1;i<nums1.size();i++){
for(int j=1;j<nums2.size();j++){
if(nums1[i]==nums2[j])
dp[i][j]=dp[i-1][j-1]+1;
//存dp[i][j]中的最大值
result=max(result,dp[i][j]);
}
}
//遍历找二维数组整个数组中的最大值,因为最大值不一定在末尾!
return result;
}
};
debug测试:解答错误
会出现这种解答错误的原因,是因为本题需要用result记录整个二维DP数组中,DP值最大的数值!
而像图中这样写,i=0,j=0状态下相等的数值,就没有记录进result里面(初始化的时候result没有计数)
因此,下面的for循环也应该从i=0,j=0开始写!为的就是方便result统计所有DP数组中的数值,从而找最大值。
思路1完整版
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>>dp(nums1.size(),vector<int>(nums2.size(),0));
//初始化
for(int i=0;i<nums1.size();i++){
if(nums1[i]==nums2[0]) dp[i][0]=1;
}
for(int j=0;j<nums2.size();j++){
if(nums2[j]==nums1[0]) dp[0][j]=1;
}
//开始遍历
int result=0;
//注意从i=0,j=0开始遍历的话,一定要在递推公式的地方进行边界检查!
for(int i=0;i<nums1.size();i++){
for(int j=0;j<nums2.size();j++){
if(nums1[i]==nums2[j]&&i>0&&j>0)
dp[i][j]=dp[i-1][j-1]+1;
//存dp[i][j]中的最大值
//不管是否有数字相等,都更新result,就是为了增加[0][j]的情况!
result=max(result,dp[i][j]);
}
}
//遍历找二维数组整个数组中的最大值,因为最大值不一定在末尾!
return result;
}
};
总结
这种思路的注意点:
dp[0][j]
和dp[i][0]
的情况都需要单独初始化- 但是单独初始化的情况并没有被result包括,因此为了包括在内,底下遍历的时候必须从0开始!就是为了
result=max(result,dp[i][j]);
这句话能够包含[0][j]
和[i][0]
的情况!
思路2:(推荐)DP数组定义为i对应nums[i-1]
DP数组含义
dp[i][j]
:长度为[0, i - 1]
的字符串text1与长度为[0, j - 1]
的字符串text2的最长重复子序列为dp[i][j]
也就是说dp[i][j]
对应的是包含nums1[i-1]
和nums2[j-1]
在内的最长重复子序列!
递推公式
//这种做法,直接从1开始就行,因为dp[i]对应nums1[i-1]
//i的结束是nums.size()
for(int i=1;i<=nums1.size();i++){
for(int j=1;j<=nums2.size();j++){
if(nums1[i]==nums2[j])
dp[i][j]=dp[i-1][j-1]+1;
result=max(result,dp[i][j]);
}
}
初始化
根据dp[i][j]
的定义,dp[i][0]
和dp[0][j]
其实都是没有意义的,此时的初始化直接初始化为0即可。
看完整版代码,如果nums1[0]==nums2[0],那么dp[1][1]=dp[0][0]+1=1
思路2完整版
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>>dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
//初始化不需要管,dp[i][0]和dp[0][j]全部置为0即可,因为没有意义
int res=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;
}
//收集所有dp[i][j]中的最大值
res = max(res,dp[i][j]);
}
}
return res;
}
};
总结
- 思路2的做法简化了初始化,使得i=0和j=0情况不需要单独对比,并且使得for循环可以正常从i=1开始防止下标越界,但仍然遍历所有数字。
由思路1和思路2的对比可以看出,这种两个序列对比的类型题目,最好还是把DP数组定义为:
以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]
。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 ,而不是考虑了[i-1]结尾的字符串!)
因为本题是找重复,找重复的话所有元素都必须比较,所以和打家劫舍那种"“考虑前i栋房屋”"的类型不一样。