【算法-LeetCode】300. 最长递增子序列(动态规划)

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

参考:最长上升子序列 - 最长递增子序列 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年8月6日21:04:24
参考:【微信公众号:代码随想录 2021-03-09】动态规划:最长递增子序列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值