300. 最长递增子序列 - 力扣(LeetCode)
发布:2021年8月6日19:23:54
问题描述及示例
给你一个整数数组 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
我的题解
没记错的话,这是我碰到的第三道动态规划的题目了吧。但是说实话,一开始我并没有想到这道题可以用动态规划解决,反而是想到了递归或者回溯之类的。但是琢磨一番之后发现好像走不通,但是又实在没有思路了,所以就看了一眼LeetCode上【相关标签】提示。要是在面试中,这样估计是过不了了,因为思路都没能自己发现。
可以看到相关标签中出现了【动态规划】的身影,于是我立马就开始往动态规划上思考了。
此时我是忽视了【二分查找】这个标签,对不起,二分,我不是故意的( ̄ェ ̄;)。
于是,根据我之前做的总结(详情请参照:【算法-LeetCode】53. 最大子序和_赖念安的博客-CSDN博客),开始一步步尝试解题。
成功前的尝试(对dp数组的错误理解)
在下面的程序中,出现了动态规划题目中常见到的dp
数组。根据总结,第一步就是确定这个dp
数组的含义,也就是确定dp[i]
表示的是什么。在我一开始的想法中,dp[i]
应该是nums[i]
之前(包括nums[i]
)的最长递增子序列的长度(事实上也确实应该这样设定)。但是写着写着,我就忘了初心(后来回想,似乎就是从dp[0]
的初始化开始走歪的),没有紧扣dp[i]
所表示的意义,开始将dp
数组用来存储最长递增子序列本身,而非其长度。所以说,这动态规划的第一步我就出了问题。详解请看下方注释。
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function (nums) {
// 首先将dp数组初始化为空数组
let dp = [];
// 初始化dp数组,此乃万恶之源
dp[0] = nums[0];
// 由下标为1处开始遍历nums数组,遍历顺序为由前向后
for (let i = 1, len = nums.length; i < len; i++) {
// 如果dp数组为空,或者当前遍历的数组元素比dp数组的末尾元素大(注意题目中说是严格递增
// 子序列,所以不要取等号),则将当前遍历元素压入数组,且跳过本次循环的后续操作
if (dp.length === 0 || nums[i] > dp[dp.length - 1]) {
dp.push(nums[i]);
continue;
}
// 如果dp数组不为空,或者当前遍历的数组元素不比dp数组的末尾元素大,则将dp数组的末尾元素弹出
dp.pop();
// 弹出之后,也要记得将当前遍历数组压入dp数组,但是压入之前要判断dp数组前是否已经包含该元素
if (!dp.includes(nums[i])) {
dp.push(nums[i]);
}
}
// 遍历完成后,dp数组中就存储了nums数组中的最长严格递增子序列,返回其长度即可
return dp.length;
};
我以为上面的程序已经可以通过测试用例了,提交之后发现,当nums = [0, 1, 0, 3, 2, 3]
时,所得的结果就是3
,而非预期中的4
,将程序放到Chrome开发者工具中debug,发现问题就出现在dp.pop();
身上,因为当遍历到nums[2]
时,由于nums[2] < dp[dp.length - 1]
(此时dp
为[0, 1]
)且dp.length !== 0
,所以程序就会将dp[1]
弹出,但是我们可以通过观察发现,其实这个元素1
不应该被弹出。所以问题就卡在这儿了。
我的题解1(动态规划)
严格来说这并不是我的题解,因为关键的状态转移方程没有成功推导出来。
实在想不清楚该怎么改上面的程序了,所以只好推倒重来了。当我再次重复动态规划的解题步骤时,我突然就发现我没有明确dp
数组的含义。这次我就坚守初心:dp[i]
应该是nums[i]
之前(包括nums[i]
)的最长递增子序列的长度,而最后返回的结果应该是dp
数组中的最大值。但是又出现了新的问题:那状态转移方程是啥呢?这里我一开始只有一个模糊的概念,dp[i]
应该是由dp[i-1]
再拼接上nums[i]
得到的,但是得事先判断nums[i]
和dp[i-1]
里的元素的大小关系啊。
其实到这里我已经很接近答案了,但是很可惜的是,到最后我还是没有写出状态转移方程,于是我就开始去看了别人的题解,详情可参考【有关参考】,感谢【微信公众号:代码随想录】以及其他博主的分享与解析。
除了状态转移方程和我原先的思路不大一样外,博主的dp
数组的初始化也不一样。详解请看下方注释。
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
// 首先将dp初始化为一个长度与nums一样,而元素值均为1的数组,因为子序列长度起码有1
let dp = new Array(nums.length).fill(1);
// result用于存储dp数组中的最大值并最后作为返回值返回
let result = 1;
// 这句其实可以不写,但是为了动态规划题目的步骤完整性和格式化,所以加上
dp[0] = 1;
// 外层for循环用于由前向后遍历nums数组
for(let i = 1, len = nums.length; i < len; i++) {
// 内层for循环用于确定nums[i-1]之前(包括nums[i-1])的最长递增子序列,
// 也就是找到dp[0] ~ dp[i-1]中的最大值
for(let j = 0; j < i; j++) {
// 如果当前遍历的外层for循环元素nums[i]比当前遍历的内层for循环元素nums[j]小,
// 则直接不考虑将nums[i]纳入dp[i]的计算中,否则就取dp[j]+1和dp[i]中的较大值,
// 当内层for循环结束后,就能保证dp[i]是保存的是最长递增子序列的长度
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 内层for循环结束后,要更新result的值
result = Math.max(result, dp[i]);
}
return result;
};
提交记录
54 / 54 个通过测试用例
状态:通过
执行用时:168 ms, 在所有 JavaScript 提交中击败了69.37%的用户
内存消耗:39.7 MB, 在所有 JavaScript 提交中击败了15.52%的用户
时间:2021/08/06 19:27
在上面的程序中,内层for
循环也是关键的动态转移方程的一部分,一定要注意不能只看到Math.max(dp[i], dp[j] + 1)
就认为是取dp[i]
和dp[j] + 1
中的最大值,不能忽略这个语句是在内层for
循环中,所以,经过至多i-1
次的Maht.max()
操作后,我们实际上是拿dp[x]
(0 ≤ x ≤i -1
)中的最大值与dp[i]
作比较。只不过上面的程序中将【取dp[x]
(0 ≤ x ≤i -1
)中的最大值】和【dp[x]
的最大值与dp[i]
作比较】杂糅在一起写并同时加了一个nums[i] > nums[j]
的判断。
在看官方题解的过程中,我还看到了一种利用【二分查找】的思想的解法,但是这里暂且按下不表,因为我还没怎么看懂[doge]。
官方题解
更新:2021年7月29日18:43:21
因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。
更新:2021年8月6日19:29:12
【更新结束】
有关参考
更新:2021年8月6日21:04:24
参考:【微信公众号:代码随想录 2021-03-09】动态规划:最长递增子序列