Leetcode 300:最长上升子序列(最详细的解法!!!)

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

解题思路

首先想到的解法就是找出所有的子序列,然后对每个子序列进行判断,如果是上升子序列我们就记录高度,最后我们取所有高度中的最大值即可。关于所有子集的问题,可以看这两篇Leetcode 78:子集(最详细的解法!!!)Leetcode 90:子集 II(最详细的解法!!!) 。但是我们这里没有使用上述的代码实现,我使用了内建函数itertools.combinations

import itertools
class Solution:
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        all_list = list()
        len_nums = len(nums)
        for i in range(1, len_nums + 1):
            all_list.extend([list(per) for per in itertools.combinations(nums, i)])
        
        result, flag = 0, 1
        for per_list in all_list:
            for i, _ in enumerate(per_list[:-1]):
                if per_list[i] > per_list[i + 1]:
                    flag = 0
                    continue
            if flag:
                result = max(result, len(per_list))

            flag = 1

        return result

但是这个算法显然是不合理的,时间复杂度太高了。有没有更快的?实际上这是一个动态规划可以解决的问题。我们定义这样的一个函数 f ( x ) f(x) f(x)表示以 n u m s [ x ] nums[x] nums[x]数字结尾的最长子序列的长度,其中 x x x表示 n u m s nums nums中的位置。

x x x位置的函数值由 x x x之前的 m a x ( f ( j ) )   j < x   a n d   n u m s [ x ] > n u m s [ j ] max(f(j))\ j < x\ and \ nums[x]>nums[j] max(f(j)) j<x and nums[x]>nums[j]决定。所以

  • f ( i ) = 1 + m a x ( f ( j )   i f   n u m s [ i ] > n u m s [ j ] )   ( j < i ) f(i) = 1 + max(f(j)\ if\ nums[i] > nums[j])\ (j < i) f(i)=1+max(f(j) if nums[i]>nums[j]) (j<i)

通过整个方程,可以快速的写出代码

class Solution:
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0

        len_nums = len(nums)
        mem = [1]*len_nums
        result = 1
        
        for i in range(1, len_nums):
            for j in range(i):
                if nums[j] < nums[i]:
                    mem[i] = max(mem[i], 1 + mem[j])
            result = max(result, mem[i])
        return result

这是我们大多数人可以想到的解法,实际上这不是最快的算法,这个算法的时间复杂度是O(n^2),我们还有比这个更快的算法。

我们建立一个临时数组tmp(用于存放一组最长上升子列),首先将nums[0]插入其中,然后遍历nums[1:]

  • 如果遍历到的元素val <= tmp[0],我们更新tmp[0]=val(也就是遍历到的元素比最长上升子序列中的最小元素小,此时按照贪心的策略,当然是越小越好,所以替换之前元素)
  • 如果tmp[0] < val <= tmp[-1],我们通过二分搜索法找到第一个>=val的元素位置,然后用val替换掉它(和上面的做法思路相同,找比当前位置更小的合法值)
  • 如果tmp[-1] < val,我们更新tmp.append(val)(也就是新加入的元素比最长上升子序列中的最后一个元素大,那么必然构成解)

一题目中的例子为例,我们建立一个tmp=[10],然后我们发现10 < 9,所以我们

tmp: [9]

然后我们考虑9 < 2,所以

tmp: [2]

然后我们考虑2 < 5,所以

tmp: [2 5]

接着往后5 > 3 && 2 < 5,所以

tmp: [2 3]

3 < 7,所以

tmp: [2 3 7]

基于这个思想,我们可以写出下面的代码

class Solution:
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        mem = list()
        len_nums = len(nums)
        for i in range(len_nums):
            low, upper = 0, len(mem)
            while low < upper:
                mid = (upper - low)//2 + low
                if mem[mid] < nums[i]:
                    low = mid + 1
                else:
                    upper = mid

            if upper == len(mem):
                mem.append(nums[i])
            else:
                mem[upper] = nums[i]
        
        return len(mem)

实际上上面的写法还可以简化为

from bisect import bisect_left
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        mem = list()
        n = len(nums)
        for i in range(n):
            index = bisect_left(mem, nums[i])
            if len(mem) == index:
                mem.append(nums[i])
            else:
                mem[index] = nums[i]
        return len(mem)

reference:

https://leetcode.com/problems/longest-increasing-subsequence/discuss/152065/Python-explain-the-O(nlogn)-solution-step-by-step

我将该问题的其他语言版本添加到了我的GitHub Leetcode

如有问题,希望大家指出!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值