
最长上升子序列是动规的经典题目,这里dp[i]是可以根据dp[j] (j < i)推导出来的,那么依然用动规五部曲来分析详细一波:
1.dp[i]的定义
dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
2.状态转移方程
位置i的最长升序子序列等于 j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要 dp[i] 与 dp[j] + 1 进行比较,而是我们要取dp[j] + 1的最大值,这个是通过for循环来实现的。
3.dp[i]的初始化
每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1.
4.确定遍历顺序
dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是0到i-1,遍历i的循环在外层,遍历j则在内层,代码如下:
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]; // 取长的子序列
}
5.举例推导dp数组
输入:[0,1,0,3,2],dp数组的变化如下

直接给出代码如下:
int lengthOfLIS(vector<int> &nums)
{
if(nums.size() <= 1)
return nums.size();
int result = 0; // 最终结果
// 1.定义dp数组
vector<int> dp(nums.size(), 1);
// 2.动态规划,计算前面的每一个子串的最长序列
for(int i = 1; i < nums.size(); i++)
{
// 对这个子串,计算它前面的所有子串的最大长度
for(int j = 0; j < i; j++)
{
// 当前位置元素 > 子串的最后一个元素,则递增序列长度可以+1
if(nums[i] > nums[j])
{
dp[i] = max(dp[i], dp[j] + 1); // 和上一次的比,取最大值
}
}
// 找到了以i结尾的子串的最大递增长度,求最终的最大递增长度
if(dp[i] > result)
result = dp[i];
}
return result;
}
注意:这道题目感觉很简单,但是感觉还是没有完全理解。注意以下两点:
i结束的子串的最大递增长度的时候,需要从前面的所有子串的最大递增长度中挨个对比,找到最大的递增长度dp.back(),而是要在dp数组中寻找最大值。所以在代码中for(i)的循环中就一直在用result记录最大值。注意:这道题目自己的疑问可能就是在于,为什么在当前i的位置找最长的递增序列的时候,遍历i之前的所有子串j,然后nums[i] > nums[j]的话,就可以把最长的递增序列+1呢?有可能会nums[j]并不是递增的数啊,也就是0-j的递增的子序列长度和j无关,是0 - (j-1)的子序列的递增长度,所以现在如果nums[i] > nums[j]的话,未必可以保证nums[i]也是相比0 - (j-1)的子序列递增的啊?
解答:其实是因为前面自己对dp数组的理解出现了问题!(不过代码随想录中也没说清楚,他说的是dp[i]是i之前包括i的以nums[i]结尾最长上升子序列的长度)。其实这里的dp数组虽然仍然是上升子序列,但是要求这个子序列必须是以nums[i]为结尾的,也就是这个递增子序列中必须有nums[i]!所以说,只要nums[i] > nums[j],由于dp[j]是以nums[j]为结尾的最长递增子序列的长度,所以现在nums[i] > nums[j]之后,那么以nums[i]为结尾的最长递增子序列的长度就一定是dp[j]+1。这样我们把所有的nums[i]结尾的子序列遍历完成之后,最长的子序列就是这些子序列的最长长度中的最大值!
如下所示,数组和以他们为结尾的最长子序列的长度。所以最后的结果就是从所有的最长递增子序列中寻找一个最大的值,也就是寻找dp[i]数组中的最大值,这也就是代码中if(dp[i] > result) result = dp[i]的作用,就是在寻找dp数组中的最大值。因此,dp数组的含义是:以nums[i]为结尾、必须包含nums[i]的最长递增子序列的长度。

注意:如下的理解是错误的,就是认为dp[i]是从0-i的子序列的最大递增长度,未必包含nums[i]。这样的话问题就出现在无法使用递推公式进行递推了。比如nums[0] - nums[4]的最大递增长度是5,现在nums[5] > nums[4],但是怎么知道nums[0] - nums[5]的最大递增长度呢?没法知道,关键因素就是如果按照这样定义dp数组,最大递增长度中不一定含有nums[i],这样就导致没法判断最长递增序列的最后一个数是多少了,所以就没法对比了。

本题相对于 动态规划:300.最长递增子序列 最大的区别在于“连续”。
动规五部曲分析如下:
1.确定dp数组(dp table)以及下标的含义
dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。注意:和昨天的题目一样,这个递增序列中一定是包含nums[i]的,这样后面才能使用递推公式,原因分析见上一题的最后自己写的笔记。
注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。注意:这里不一定以nums[0]为起始位置很重要,比如1, 2, 5, 3, 4,遍历到3的时候发现不是连续递增了,所以就又重新从1开始计算连续递增子序列长度了,然后到4的时候以4结尾的连续递增子序列长度变为2。
2.确定递推公式
如果 nums[i + 1] > nums[i],那么以i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1 。即:dp[i + 1] = dp[i] + 1;
注意这里就体现出和动态规划:300.最长递增子序列的区别!因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。
注意:这里也可以借此机会再次好好理解昨天的题目。昨天的题目因为不要求是连续的子序列,所以当前的nums[i]就要和0 - (i-1)结尾的所有子序列的最后一个数进行比较,如果nums[i] > nums[j],那么就找到了一个以nums[i]结尾的递增子序列,就可以计算这个递增子序列的长度;然后遍历所有的0 - (i-1)结尾的子序列,最后就可以找到以nums[i]结尾的最长递增子序列的长度。
而本题中,由于要求必须是连续递增的子序列,所以不需要遍历之前以0 - (i-1)结尾的所有子序列了,只需要遍历nums[i-1]结尾的子序列即可。
3.dp数组如何初始化
以下标i为结尾的数组的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素,所以dp[i]应该初始1。
4.确定遍历顺序
从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。
本文在确定递推公式的时候也说明了为什么本题只需要一层for循环,代码如下:
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
dp[i + 1] = dp[i] + 1; // 递推公式
}
}
5.举例推导dp数组
已输入nums = [1,3,5,4,7]为例,dp数组状态如下:
最后给出代码如下,很简单:
int findLengthOfLCIS(vector<int> &nums)
{
if(nums.size() <= 1)
return nums.size();
int result = 0; // 最终结果
// 1.定义并初始化dp数组
vector<int> dp(nums.size(), 1);
// 2.动态规划开始递推
for(int i = 1; i < nums.size(); i++)
{
if(nums[i] > nums[i-1])
{
dp[i] = dp[i-1] + 1; // 如果递增,则递增长度+1
}
else
{ }// 否则什么也不干,即重新开始一个新的递增序列,递增子序列长度重新初始化成1
// 记录最终的最长连续递增子序列
result = dp[i] > result ? dp[i] : result;
}
return result;
}

动归五部曲:
1.确定dp数组(dp table)以及下标的含义
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )
此时细心的同学应该发现,那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。
其实这个地方主要是为了后面的递推公式计算的方便,把dp[0][j]和dp[i][0],即第0行、第0列都初始化成0,方便递推公式计算,后面看dp数组的状态图就明白了。
2.确定递推公式
根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始!
3.dp数组如何初始化
根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的!
但dp[i][0] 和dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;,所以dp[i][0] 和dp[0][j]初始化为0。
举个例子A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。
4.确定遍历顺序
外层for循环遍历A,内层for循环遍历B。或者外层for循环遍历B,内层for循环遍历A。这俩没有什么区别,结果都是一样的。
同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。
代码如下:
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
5.举例推导dp数组
拿示例1中,A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下:

最后给出代码如下,注意如下代码中自己做了一些调整, 主要是i/j的意思。上面代码随想录的i/j指的是dp数组中的索引,由于dp数组为了方便递推公式增加了第0行和第0列,这样对应nums中的索引就是i-1/j-1;下面自己写的代码中i/j值得就是nums中的索引,由于dp数组为了方便递推公式增加了第0行和第0列,所以对应dp数组中的 索引是i+1/j+1。
int findLength(vector<int> &nums1, vector<int> &nums2)
{
if(nums1.empty() || nums2.empty())
return 0;
int result = 0; // 记录最终结果
// 1.定义并初始化dp数组
vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));
// 2.动态规划:类似背包问题,开始递推
for(int i = 0; i < nums1.size(); i++)
{
for(int j = 0; j < nums2.size(); j++)
{
if(nums1[i] == nums2[j])
{
// 注意这里dp[0][j]和dp[i][0]都初始化成0,没有物理意义,只是为了方便递推公式计算
dp[i+1][j+1] = dp[i][j] + 1;
}
// 更新最大长度
result = dp[i+1][j+1] > result ? dp[i+1][j+1] : result;
}
}
return result;
}