算法设计与分析(十一)

300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence.

Example

Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Note

Note:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n^2) complexity.

Follow up:

Could you improve it to O(n log n) time complexity?

思路

最近几周算法课都没有讲新的算法内容,一直在讲动态规划的习题,大概了也说明DP有多么的重要了。DP这个算法真的让人又爱又恨,它的代码量并不大,关键代码其实就状态转移方程那几行,但要找出正确的状态转移方程是一个十分伤脑筋的过程,有时候难起来真的要人命。经过这几周的练习,深深感觉到我与大佬之间的差距,知道自己有多菜,但前进的脚步不能停下来,还是要尽自己所能去挑战一些有难度的动态规划题目的,所以这周选了这一道题。

题目相当简洁明了,就是在一串乱序数组里找最长的上升序列,数字之间不需要连续,其实这也是废话,如果数字要求连续的话也就不关动态规划什么事了,简简单单把数组遍历一遍就能找到最长上升子串了。Note里说让我们先用时间复杂度为O(n^2)的方法做,这个算挺简单的,不一会就做出来。接着让我们尝试用时间复杂度O(n log n)的方法,这还是有一点难度的,不过最后还是花费了不少时间做出来了。

先来说下O(n^2)的方法吧,当前状态的最大上升子串肯定是由前面状态的最大上升子串加1得到的。因为不是重点就不详细讲了,状态转移方程和具体的代码解答如下:

j∈[0…i-1]
dp[i] = max(dp[i], dp[j] + 1) // if nums[i] > nums[j]

class Solution {
public:
  int lengthOfLIS(vector<int>& nums) 
  {
    int ans = 0;
    int index = 0;
    int *dp = new int[nums.size()];
    for (int i = 0; i < nums.size(); i++)
    {
      dp[i] = 1;
      for (int j = 0; j < i; j++)
        if (nums[i] > nums[j]) 
          dp[i] = max(dp[i], dp[j] + 1);
      ans = max(ans, dp[i]);
    }
    delete []dp;
    return ans;   
  }

  int max(int num1, int num2)
  {
    if (num1 > num2) return num1;
    else return num2;
  }
};

接下来我们来讲一下O(n log n)的解法。提到动态规划里的log n的话,首先想到的肯定是二分了。问题是怎么利用二分,排序?肯定不对,仅仅是二分排序的时间复杂度就有O(n log n)了,算上DP的时间肯定超了。那么是查找?因为题目是找上升子串,在有序数列里用二分查找,好像可行?所以我就按着这个方向去考虑了。

我们可以用一个数组把目前找到的最长上升子串存起来,当访问到一个新节点时,看情况选择把它放进数组或者是不放,判断的标准就用二分查找的结果,最后这个数组的长度就是答案。因为对每个节点我们用二分查找的结果来判断情况,不用把前面的所有状态节点遍历一遍,所以时间复杂度是O(n log n)而不是O(n^2)。判断的标准如下:

  • 如果能在数组里查找到数字num,那么保持数组状态不变
  • 如果不能找到:
    • 如果num < LIS[0],LIS[0] = num
    • 如果num < LIS[len - 1],LIS[len - 1] = num,len为数组长度
    • 其余情况,找到有序数组里第一个比num大的数字,num替代它在数组里的位置

显而易见的,按照上面的做法得到的LIS数组,里面数字的相对顺序跟原数列的相对顺序是不同的,不符合题意。然而这并不碍事,即使数字的顺序不对,它所处的位置却是对的,假如num在LIS数组的第3位,证明前面有2个数字比它小,也许经过LIS数组的更新把num后面的数字放在了它前面,但事实上确实有一个符合题意的长度为3的子串曾经在LIS数组里出现过。

O(n log n)方法的思路大概就这样了。在写代码时还有一点要注意的,因为我们二分查找的目的是找到LIS数组里第一个比num大的数字,并不是平时那样在数组里知道num的位置,因此要改变一下二分查找的函数,注意不要陷进死循环。

class Solution {
public:
  int lengthOfLIS(vector<int>& nums) 
  {
    int ans = 0;
    int index = 0;
    int *LIS = new int[nums.size()];

    for (int i = 0; i < nums.size(); i++)
    {
      index = binarySearch(nums[i], LIS, ans);
      LIS[index] = nums[i];
      if (index + 1 > ans)
        ans = index + 1;
    }

    delete []LIS;
    return ans;   
  }

  int binarySearch(const int num, int* LIS, const int& len)
  {
    if (len == 0 || num < LIS[0]) return 0;
    if (num > LIS[len - 1]) return len;
    int head = 0;
    int tail = len - 1;
    int mid;
    while (true)
    {
      mid = (head + tail) / 2;
      if (LIS[mid] == num || (LIS[mid] > num && LIS[mid - 1] < num)) break;
      else if (LIS[mid] > num) tail = mid -1;
      else head = mid + 1;
    }
    return mid;
  }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值