[Leetcode]——Longest increasing subsequence

[Leetcode]——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:
There may be more than one LIS combination, it is only necessary for you to return the length.
Your algorithm should run in O(n2) complexity.
Follow up: Could you improve it to O(n log n) time complexity?


解题思路

这是经典的最长上升子序列的问题,花了一天半的时间在考虑这个问题,当然也解决了[华为–合唱队]的问题。
这题一共有两种思路,一种是动态规划的思想,参考动态规划:最长上升子序列的解释,将其中的例子拿过来,如下:

让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。
前1个数 d(1)=1 子序列为2;
前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7
前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1
前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5
前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6
前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4
前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3
前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8
前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5

总结一下,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1(如果A[i]大于这个最长上升子序列的话),如果A[i]之前的数都大于A[i],则d[i]=1,表示由A[i]独自构成一个序列,按照这个思路写了代码,并且在LeetCode上已经编译通过了,如下所示(请注意对异常情况要进行判断,否则提交不通过):

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if len(nums) == 0 or nums is None:
            return 0
        dp = [1] * len(nums)
        result = 1
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j]+1)
            result = max(result, dp[i])
        return result

上述的算法复杂度为O(n^2),在LeetCode上跑起来时间需要1140ms,如何降低复杂度呢?同样参考动态规划:最长上升子序列的第二个例子:

我们再举一个例子:有以下序列A[]=3 1 2 6 4 5 10 7,求LIS长度。
我们定义一个B[i]来储存可能的排序序列,len为LIS长度。我们依次把A[i]有序地放进B[i]里。(为了方便,i的范围就从1~n表示第i个数)
A[1]=3,把3放进B[1],此时B[1]=3,此时len=1,最小末尾是3
A[2]=1,因为1比3小,所以可以把B[1]中的3替换为1,此时B[1]=1,此时len=1,最小末尾是1
A[3]=2,2大于1,就把2放进B[2]=2,此时B[]={1,2},len=2
同理,A[4]=6,把6放进B[3]=6,B[]={1,2,6},len=3
A[5]=4,4在2和6之间,比6小,可以把B[3]替换为4,B[]={1,2,4},len=3
A[6]=5,B[4]=5,B[]={1,2,4,5},len=4
A[7]=10,B[5]=10,B[]={1,2,4,5,10},len=5
A[8]=7,7在5和10之间,比10小,可以把B[5]替换为7,B[]={1,2,4,5,7},len=5

其实这里用到了贪心的思想,即对于一个上升子序列,显然当前最后一个元素越小,越有利于添加新的元素,这样LIS长度自然更长。但是上述讲解存在一个错误(看了很多blog上都是这么写的,不知道为什么大家都没有发现这个错误??),就是不应该对B数组进行替换操作,而应该将新的数插入到B数组中,并记录下此时对应的最长子序列,举个例子来说:

A = [1, 3, 6, 7, 9, 4, 10, 5, 6]
按照参考的blog所讲述的方法,判断出来最终的B=[1, 3, 4, 5, 6],长度为5,这是因为当判断到元素"4"时,由于4比{6, 7, 9}都要小,因此要删除{6, 7, 9},再插入4,但这种方法是错误的,显然最终的B=[1, 3, 6, 7, 9, 10],长度为6。

语言上可能难以说清楚这个来龙去脉,还是看代码吧:

class Solution3:
    def find_pos(self, dp, elem):     #用二分法查找nums[i]应该插入的位置,算法复杂度为O(logn)
        left, right = 0, len(dp) - 1
        while left <= right:
            mid = (left + right) // 2
            if dp[mid] > elem:
                right = mid - 1
            elif dp[mid] < elem:
                left = mid + 1
            else:
                return mid
        return right + 1

    def lengthOfLIS(self, nums: List[int]) -> int:
        if len(nums) == 0 or nums is None:
            return 0
        dp = [float('inf')] * len(nums)
        dp[0] = nums[0]
        res = []
        res.append(1)
        for i in range(1, len(nums)):
            if nums[i] > dp[-1]:
                dp.append(nums[i])
                res.append(len(dp))
            else:
                pos = self.find_pos(dp, nums[i])    # 找到nums[i]对应的位置,插入dp中
                dp[pos] = nums[i]
                res.append(pos + 1)       #同时记录下此时的位置,对应nums[i]之前的最长上升子序列
        return max(res)

测试了一下在LeetCode上的时间是44ms,大大降低了复杂度,另外一个比较省事的做法是在python中引入bisect包,可以实现find_pos的功能:

import bisect
class Solution2:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if len(nums) == 0 or nums is None:
            return 0
        dp = [float('inf')] * len(nums)
        dp[0] = nums[0]
        res = []
        res.append(1)
        for i in range(1, len(nums)):
            pos = bisect.bisect_left(dp, nums[i])
            res.append(pos+1)
            dp[pos] = nums[i]
        return max(res)

至此这个问题算是彻底结束了,其中很多编程的细节都需要自己去体会,且该问题经常会出现在互联网公司中的笔试题中,且会有各种变形,希望能有所收获吧!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值