文章目录
- 300.最长递增子序列
- 思路
- CPP代码
- 674.最长连续递增序列
- 思路
- CPP代码
- 718.最长重复子数组
- 思路
- CPP代码
300.最长递增子序列
力扣题目链接
文章讲解:300.最长递增子序列
视频链接:动态规划之子序列问题,元素不连续!| LeetCode:300.最长递增子序列
可以删除或不删除某些元素,保证数组原有的顺序,然后求最长的递增子序列。
这是典型的子序列系列,也是卡哥安排的第一个动规子序列问题。
思路
dp[i]
的定义
dp[i]
表示i
之前包括i
的以nums[i]
结尾的最长递增子序列的长度
- 递推公式
如果j < i
,并且有nums[j] < nums[i]
,其中,以nums[j]
结尾的最长递增子序列长度为dp[j]
。以nums[i]
结尾的最长递增子序列长度为dp[i]
。
我们应该有dp[i]=dp[j] + 1
,再者,我们会遍历每一个小于i
的下标j
(也就是说我们会固定i
,去遍历j
),所以,我们的递推公式是:
dp[i] = max(dp[i], dp[j] + 1)
- dp数组的初始化
每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.
- 确定遍历顺序
老样子,从前到后遍历,至于j
的遍历范围是~i-1
,但是遍历方向都无所谓.
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]; // 取长的子序列
}
这里为什么要定义一个result
呢,因为我们如果不保存结果的话,后面还得去遍历dp
数组找最大,着实没必要
- 举例推导dp数组
输入:[0,1,0,3,2],dp数组的变化如下:
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;
}
};
674.最长连续递增序列
力扣题目链接
文章讲解:674.最长连续递增序列
视频讲解:动态规划之子序列问题,重点在于连续!| LeetCode:674.最长连续递增序列
状态:连续递增子序列和递增子序列区别在哪里?体现在代码中的话又在哪里呢?
来了,本题要求是连续序列,而不是原始序列派生的子序列
思路
- 明确dp数组的含义
以下标i为结尾的最长连续递增子序列的长度为dp[i]
- 确定递推公式
在300.最长递增子序列中,我们的dp[i]
是由i
面前的某个元素j
来进行推导。
本题中我们求的是连续的递增子序列,所以我们没有必要去比较前面的所有元素了,在上一题中,我们可是遍历了0~i-1
的j
。
所以如果我们nums[i] > nums[i-1]
,我们就做对应的那个dp[i] + 1
的操作,即:
dp[i]=dp[i-1]+1
- dp数组的初始化
以下标i为结尾的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。
所以dp[i]应该初始1;
- 确定遍历顺序
从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
}
- 举例推导dp数组
已输入nums = [1,3,5,4,7]为例,dp数组状态如下:
CPP代码
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if (nums.size() == 0) return 0;
int result = 1;
vector<int> dp(nums.size() ,1);
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
if (dp[i] > result) result = dp[i];
}
return result;
}
};
718.最长重复子数组
力扣题目链接
文章讲解:718.最长重复子数组
视频讲解:动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组
本题要操作两个数组了,就是要求两个数组中最长的重复子数组长度。
这里的子数组呢其实就是连续子序列,强调的是连续
暴力解法:两层for循环确定两个数组起始位置,然后再来一个循环可以是for或者while,来从两个起始位置开始比较,取得重复子数组的长度。
思路
- dp数组含义
dp[i][j]
:以下标i - 1
为结尾的num1
,和以下标j - 1
为结尾的num2
,最长重复子数组长度为dp[i][j]
。
为什么要定义成i-1
结尾和以j-1
结尾呢?
其实是为了让后续代码尽可能简洁。后续的话会写如果定义成i
结尾和j
结尾的代码麻烦之处
- 递推公式
根据dp[i][j]
的定义,dp[i][j]
的状态只能由dp[i - 1][j - 1]
推导出来。
即当nums[i - 1]
和nums2[j - 1]
相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1
根据递推公式可以看出,遍历i 和 j 要从1开始!
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
- 初始化
为了使递推公式能够完成推导,dp[i][0]
和dp[0][j]
初始化为0。
举个例子nums1[0]
如果和nums2[0]
相同的话,dp[1][1] = dp[0][0] + 1
,只有dp[0][0]
初始为0
,正好符合递推公式逐步累加起来。
- 遍历顺序
从小到大遍历即可,先遍历哪个也都是可以的,并且在遍历的过程中记录dp[i][j]
的最大值
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;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
- 打印
拿nums1: [1,2,3,2,1],nums2: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下:
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;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
//滚动数组,遍历nums2的时候,要从后向前遍历,避免重复覆盖
// 版本二
class Solution {
public:
int findLength(vector<int>& A, vector<int>& B) {
vector<int> dp(vector<int>(B.size() + 1, 0));
int result = 0;
for (int i = 1; i <= A.size(); i++) {
for (int j = B.size(); j > 0; j--) {
if (A[i - 1] == B[j - 1]) {
dp[j] = dp[j - 1] + 1;
} else dp[j] = 0; // 注意这里不相等的时候要有赋0的操作
if (dp[j] > result) result = dp[j];
}
}
return result;
}
};