题目1:300.最长上升子序列
1.确定dp数组以及下标的含义
dp[i]表示 从[0,i]中 以nums[i]结尾的最长上升子序列 的 长度
2.确定状态转移方程
如果一个上升子序列后接了一个较大数,那么就能构成一个更长的上升子序列。
因此如果nums[i]>nums[j] (j<i),且有dp[j] (dp[j]为[0,i-1]的最长上升子序列)的值,那么[0,i]的最长升序子序列 等于 j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
状态转移方程为:dp[i] = max(dp[i],dp[j]+1)
3.dp数组初始化
dp[i] = 1,即一个字符就是长度为1的上升子序列
4.确定遍历顺序
从前向后遍历
5.举例推导dp数组
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 = 0; i < nums.size(); i++) { //i也可以直接从1开始,因为j<i,就算i从0开始也会直接跳到i++
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[i] + 1);
}
if (dp[i] > result) result = dp[i]; //这里就是找dp[i]数组中的最大值
}
return result;//return *max_element(dp.begin(), dp.end());
}
};
仔细推敲代码就会发现其实是有双指针思想的
题目2:674.最长连续递增序列
这一题和300相比多了个连续,即求不被其他数字隔开的最长递增子序列
1.确定dp数组以及下标的含义
dp[i]表示 数组[0,i]中的 最长连续上升子序列 的 长度
2.确定状态转移方程
如果 nums[i + 1] > nums[i],那么以 i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1
即dp[i + 1] = dp[i] + 1
这里除了状态转移方程和300不一样之外,本题只需要用一层for循环遍历,因为比较的是连续的两个数nums[i+1]和nums[i]
3.dp数组初始化
dp[i] = 1,即一个字符就是长度为1的上升子序列
4.确定遍历顺序
从前向后遍历
5.举例推导dp数组
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 = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) dp[i + 1] = dp[i] + 1;
else if (dp[i + 1] > result) result = dp[i + 1];
}
return result;
}
};
题目3:718.最长重复子数组
感觉这道题代码随想录的解题思路有些牵强了,有需要的可以看评论区大佬的题解:「手画图解」动态规划 思路解析 | 718 最长重复子数组
1.确定dp数组以及下标的含义
dp[i][j]表示 长度为i,末尾项为nums1[i-1]的数组,与长度为j,末尾项为nums2[j-1]的数组,二者的 最长公共后缀子数组长度。
2.确定状态转移方程
在两个数组中各抽出一个前缀数组,比较两个前缀数组的末尾项,
如果末尾项不相同,则以它们为末尾项的公共子数组长度为0,即dp[i][j]=0;
如果末尾项相同,则以它们为末尾项的公共子数组长度起码为1,即dp[i][j]>=1,这时就要再考虑它们的前缀数组:
如果它们俩前缀数组的末尾项不相同,那么前缀数组提供的的公共长度为0,即dp[i-1][j-1]=0(这里并不是说前缀数组的公共长度为0),则dp[i][j]=1
如果它们俩前缀数组的末尾项相同,那么前缀数组提供的公共长度至少为1,即dp[i-1][j-1]>=1,则dp[i][j] = dp[i-1][j-1] + 1
3.dp数组初始化
如果 i==0 || j == 0,则二者没有公共部分,dp[0][0]=0
记录dp[i][j]最大值,即最长的公共子数组长度
4.确定遍历顺序
都可以,本题用外层for循环遍历nums1,内层for循环遍历nums2。
5.举例推导dp数组
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;
}
};
题目4:1143.最长公共子序列
和718相比,本题不再求连续的公共子序列,求相对顺序相同的公共子序列,但是其实思路可是借鉴
在题解区也有位大佬讲的非常清楚,结果718的题解应该就非常明了了:最长公共子序列 | 图解DP | 最清晰易懂的讲解 【c++/java版本】
1.确定dp数组以及下标的含义
定义 f[i][j]表示字符串text1的[1,i]区间和字符串text2的[1,j]区间的最长公共子序列长度(下标从1开始)
2.确定状态转移方程
若text1[i] == text2[j] ,
即两个字符串的最后一位相等,那么问题就转化成了字符串text1的[1,j-1]区间和字符串text2的[1,j-1]区间的最长公共子序列长度再加上一,即dp[i][j] = dp[i - 1][j - 1] + 1
。
若text1[i] != text2[j],
即两个字符串的最后一位不相等,那么字符串text1的[1,i]区间和字符串text2的[1,j]区间的最长公共子序列长度无法延长,
因此dp[i][j]就会继承dp[i-1][j]与dp[i][j-1]中的较大值,即dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
。
因此状态转移方程为:
当text1[i] == text2[j],dp[i][j] = dp[i - 1][j - 1] + 1;
当text1[i] != text2[j],dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
3.dp数组初始化
f[i][0] = f[0][j] = 0,空字符串与有长度的字符串的最长公共子序列长度为0
注:
我们定义的状态表示dp数组和text数组下标均是从1开始的,而题目给出的text数组下标是从0开始的,为了一 一对应,在判断text1和text2数组的最后一位是否相等时,往前错一位,即使用text1[i - 1]和text2[j - 1]来判断。
为什么f数组和text数组均定义成下标从1开始:
因为状态转移方程 f[i][j] = max(f[i - 1][j],f[i][j - 1]), dp数组定义成下标从1开始以后,就可以在代码中不用对下标越界问题做出额外判断。
题目给定的原数组,比如text数组,如果下标从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 = 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()];
}
};
题目5:1035.不相交的线
这道题的关键在于理解题意
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交
其实就是求两个字符串的最长公共子序列的长度
这么一转变其实就是和1143是一个题,甚至代码都不用变
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 (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]);
}
}
return dp[nums1.size()][nums2.size()];
}
};
题目6:53.最大子序和
1.确定dp数组以及下标的含义
dp[i]表示以nums[i]结尾的最大连续子序列的和。
2.确定状态转移方程
以 nums[i] 结尾的连续子数组与以 nums[i - 1] 结尾的连续子数组只相差一个元素 nums[i] 。
假设数组 nums 的值全都严格大于 00,那么一定有 dp[i] = dp[i - 1] + nums[i]。
但dp[i-1]有可能是负数,此时dp[i] = nums[i]
因此状态转移方程为dp[i] = max(dp[i-1] + nums[i], nums[i])
3.dp数组初始化
dp[0]=nums[0],即最小的子序列的和
4.确定遍历顺序
递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。
5.举例推导dp数组
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 0) return 0;
vector<int> dp(nums.size());
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值
}
return result;
}
};