Leetcode 300.最长递增子序列
题目链接:300 最长递增子序列
题干:给你一个整数数组
nums
,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,
[3,6,2,7]
是数组[0,3,1,6,2,2,7]
的子序列。
思考:动态规划。难点:如何将问题看成连续重复子问题。本题给定数组长度为处理区间,如果固定数组首个元素作为子处理区间头部,从第二个元素依次向后移动作为子处理区间尾部以及递增子序列的最大值,统计每个递增子序列的长度并比较,其中最大值便是题目要求的。
- 确定dp数组以及下标的含义
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为做 递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾 和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么 如何算递增呢。
- 确定递推公式
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
- dp数组如何初始化
每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1(即nums[i]这一个元素)。
- 确定遍历顺序
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。
而内层循环 遍历数组其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要将 0 到 i-1 的元素都遍历一次即可。 (下面代码采取从前向后遍历)
遍历i的循环在外层,遍历j则在内层,代码如下:
- 举例推导dp数组
举例:[0,1,0,3,2],dp数组的变化如下:
代码:
class Solution {
public:int lengthOfLIS(vector<int>& nums) {vector<int> dp(nums.size(), 1); //dp[i]:以nums[i]结尾的最长严格递增子序列长度int result = 1; //记录最大长度for (int i = 1; i < dp.size(); i++) { //遍历子序列区间for (int j = 0; j < i; j++) { //遍历整数数组if (nums[j] < nums[i]) //保持严格递增dp[i] = max(dp[i], dp[j] + 1);}result = max(result, dp[i]);}return result;}
};
Leetcode 674. 最长连续递增序列
题目链接:674 最长连续递增序列
题干:给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标
l
和r
(l < r
)确定,如果对于每个l <= i < r
,都有nums[i] < nums[i + 1]
,那么子序列[nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
思考一:动态规划。
- 确定dp数组(dp table)以及下标的含义
dp[i]:以下标i为结尾的连续递增的子序列长度。
- 确定递推公式
如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增的子序列长度 一定等于 以i - 1为结尾的连续递增的子序列长度 + 1 。即:dp[i] = dp[i - 1] + 1;
因为本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。
因此也不用两层for循环,本题一层for循环就行,比较nums[i] 和 nums[i - 1]。
- dp数组如何初始化
以下标i为结尾的连续递增的子序列长度最少也应该是1即nums[i]这一个元素。所以dp[i]应该初始1
- 确定遍历顺序
从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。
- 举例推导dp数组
举例:nums = [1,3,5,4,7],dp数组状态如下:
代码:
class Solution {
public:int findLengthOfLCIS(vector<int>& nums) {vector<int> dp(nums.size(), 1); //dp[i]:以nums[i]结尾的最长最长连续递增子序列长度int result = 1; //记录最大长度for (int i = 1; i < nums.size(); i++) {if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1; //连续递增则长度加一if (result < dp[i]) result = dp[i]; //更新最大长度}return result;}
};
思考二:贪心法。只要遇到nums[i] > nums[i - 1]的情况,count就++,否则count为1,记录count的最大值就行。
代码:
class Solution {
public:int findLengthOfLCIS(vector<int>& nums) {int result = 1; //记录最大长度int count = 1; //记录当前子序列长度for (int i = 1; i < nums.size(); i++) { //遍历数组if (nums[i] > nums[i - 1])count++; //连续递增则长度加一elsecount = 1; //不连续递增则重置长度if (result < count) result = count; //更新最大长度}return result;}
};
Leetcode 718. 最长重复子数组
题目链接:718 最长重复子数组
题干:给两个整数数组
nums1
和nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100
思考:动态规划。此题如果暴力解,两个for循环内部再加个while循环,时间复杂度大。可以选用空间换时间,用二维数组可以记录两个字符串的所有比较情况。
- 确定dp数组(dp table)以及下标的含义
dp[i][j] :以下标为nums1[i - 1]和nums2[j - 1]结尾的最长公共子数组长度
从定义可以看出,在遍历dp[i][j]的时候i 和 j都要从1开始。
为什么将dp[i][j] 定义为 以下标为nums1[i]和nums2[j]结尾的最长公共子数组长度,下面初始化会解释。
- 确定递推公式
从dp[i][j]的定义可以看出,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当nums1[i - 1] 和nums2[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
由递推公式可以看出,遍历dp[i][j]的时候i 和 j都要从1开始。
- dp数组如何初始化
从dp[i][j]的定义可以看出,dp[i][0] 和dp[0][j]其实都是没有意义的。
那为什么上面这么定义dp数组?
从递推公式来看如果 将dp[i][j] 定义为 以下标为nums1[i]和nums2[j]结尾的最长公共子数组长度 那么初始化操作要先将dp数组第一行和第一列优先处理,再进行循环处理后面的dp数组。代码不简洁。
由递推公式可以看出,dp数组的每个元素都可初始化为0。
- 确定遍历顺序
外层for循环遍历nums1,内层for循环遍历nums2 或 外层for循环遍历nums2,内层for循环遍历nums1都行。
同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。
- 举例推导dp数组
举例:nums1: [1,2,3,2,1],nums2: [3,2,1,4,7],dp数组的状态变化如下:
代码:
class Solution {
public:int findLength(vector<int>& nums1, vector<int>& nums2) {//dp[i][j]:以下标为nums1[i - 1]和nums2[j - 1]结尾的最长公共子数组长度vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));int result = 0; //记录公共子数组最大长度for (int i = 1; i <= nums1.size(); i++) { //遍历数组1for (int j = 1; j <= nums2.size(); j++) { //遍历数组2if (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;}
};
自我总结:
动态规划dp数组的定义要考虑到各种情况来定义,以及将处理区间逐步增大的思路。