LeetCode 300 最长上升子序列

题目描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。
例如:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

解析

暴力解法

本题很容易想到最暴力的解法,那就是找到所有的子序列,逐一判断是否是上升子序列,并记录长度,从而得到最长上升子序列的长度。显然,序列中每个元素都有“加入子序列”或“不加入”两种状态,因此对于长度为N的序列,其子序列数量为2^N(包含空序列),这样的复杂度实在是难以接受。

O(N^2)动态规划解法

由于我们需要求的是最长上升子序列的长度,因此非上升子序列其实并没有必要讨论,而且子序列的元素索引也要保持递增。

因此如果我们想知道以第i个元素结尾的最长上升子序列有多长,就需要遍历0到i-1的元素,如果其中第j个元素小于这里的第i个元素,那意味着以第i个元素结尾的最长上升子序列至少是以第j个元素结尾的最长上升子序列长度再加1.按照顺序方式计算以每个元素结尾的最长上升子序列长度,就得到了时间复杂度为O(N^2)的动态规划解法。

我们需要一个与输入数组等长的数组dp,dp[i]就表示以第i个元素结尾的最长上升子序列的长度。显然dp[0]=1.
状态转移方程为:
d p [ i ] = m a x ( d p [ j ] ) + 1 , j ∈ { 0 ≤ j < i , n u m s [ j ] < n u m s [ i ] } dp[i] = max(dp[j])+1, j\in \{0\leq j<i, nums[j]<nums[i]\} dp[i]=max(dp[j])+1,j{0j<i,nums[j]<nums[i]}
其中nums为输入的原数组。
最长上升子序列必然以其中一个元素结尾,因此dp数组的最大值即为所求。
根据这一思路,可以写出C++代码如下:

int lenOfLIS(vector<int>&nums) {
  if (nums.empty()) return 0;
  int len = nums.size();
  int maxL = 1;
  vector<int>dp(len, 1);
  for (int i = 1; i < len; ++i) {
    for (int j = 0; j < i; ++j) {
      if (nums[j] < nums[i]) {
        dp[i] = max(dp[i], dp[j] + 1);
      }
    }
    maxL = max(maxL, dp[i]);
  }
  return maxL;
}

O(N*logN)解法

上述动态规划解法关注了以每个元素结尾的最长上升子序列的长度,实际上,我们也可以这样维护一个当前的最长上升子序列,这样我们不需要遍历当前元素之前的所有元素结尾的长度,只需要找到当前元素能否加入这一子序列以及加入的位置,这样遍历全部元素后计算这一子序列长度即可。
首先我们定义一个数组lis, 初始时只包含原数组的第一项nums[0]。接下来遍历原数组,如果遍历到第i个元素,他比lis中的最后一项(最大值)还要大,那么直接在lis尾部插入这一元素;否则,我们找到这个lis数组中大于nums[i]的最小元素(第一个大于nums[i]的元素),并用nums[i]的值替换掉。

这种思路简而言之,就是按顺序遍历数组,如果当前元素大于所维护的上升子序列的最大元素,意味着这个元素可以跟之前的子序列一起构成更长的子序列;反之,可以将维护子序列中的第一个大于该元素的值用这个元素替换掉,意味着这个元素的出现可以在之后的遍历中为插入上升子序列“降低门槛”。
以例题做一个参考:
初始状态
nums: [10,9,2,5,3,7,101,18]
lis: [10]
接下来遍历到数字‘9’由于9<10则10被替换
lis: [9]
然后是’2’,同理替换掉了原来的9
lis: [2]
接下来是5直接插入尾部
lis: [2,5]
接下来的3会替换掉5
lis: [2,3]
遇到7直接插入
lis: [2,3,7]
101直接插入:
lis: [2,3,7,101]
18替换101
lis: [2,3,7,18]
遍历完成,最长子序列可能不唯一最后的lis只是一种结果,但长度都是一样的。
在寻找“大于nums[i]的最小元素”这一过程中,可以通过二分查找将这部分时间复杂度降低到O(logN),因此这个解法总的时间复杂度为O(N*logN)
C++代码如下:

int lenOfLIS2(vector<int>&nums) {
  if (nums.empty()) return 0;
  int len = nums.size();
  vector<int>lis;
  lis.push_back(nums[0]);
  int maxL = 1;
  for (int i = 1; i < len; ++i) {
    if (nums[i] > lis.back()) {
      lis.push_back(nums[i]);
    }
    else {
      int left = 0, right = maxL - 1, mid;
      while (left < right) {
        mid = left + (right - left) / 2;
        if (lis[mid] < nums[i])left = mid + 1;
        else right = mid;
      }
      lis[right] = nums[i];
    }
    maxL = lis.size();
  }
  return maxL;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值