0. 题目
给你一个整数数组 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
1. 基本思路
我们开一个长度为len(nums)的数组dp,将dp中所有元素初始化为 1 (子序列的最小长度为1),dp[i] 表示以第 i 个元素为结尾的最大递增子序列的长度。
则我们可以写出状态转移方程,对于dp[i]:
dp[i] = max(dp[j])+1 nums[i]>nums[j] 且 0<=j<i
之后利用递推方程依次求出dp所有值,之后输出max(dp)即可
空间复杂度O(n),时间复杂度O(n^2)
2. 代码
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
#一定要记得判断下输入是否为空数组,有些题目会设置奇怪的测试样本
if not nums:
return 0
n=len(nums)
dp=[1]*n
for i in range(n):
for j in range(i):
if nums[i]>nums[j]:
dp[i]=max(dp[i],dp[j]+1)
return max(dp)
3. 进阶思路
常规思路中,遍历一遍nums(dp)需要O(n)的时间复杂度,求解每个dp[i]需要O(n)的时间复杂度,总的时间复杂度为O(n^2),遍历nums不可避免,即其对应的O(n)的时间复杂度无法优化,我们只能从dp[i]入手。
为了使序列最长,我们希望序列增大的尽可能慢。可以开辟一个数组tails,tails[i]用来存储长度为i+1的递增序列的末位数值(即序列中的最大值),为了满足序列增大的尽可能慢这个条件,我们希望递增子序列的末尾数值尽可能小。同时,由tails的定义可知,tails一定递单调递增。对于递增/递减的序列,可以采用二分搜索。
遍历nums中的每一个num,若num>tails中的所有值,则递增子序列的长度加一,并将num加入tails中。若tails中有大于num的数,找到最小的大于num的数,并将这个数变更为num。
这样,遍历完整个数组后即可得到问题的解。
遍历nums的时间复杂度为O(n),二分搜索的时间复杂度为O(logn),总的时间复杂度为O(nlogn)
4. 代码
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if len(nums)<=1:
return len(nums)
tails,res=[0]*len(nums),0
for num in nums:
left,right=0,res
while left<right:
mid=(left+right)//2
if tails[mid]<num:
left=mid+1
else:
right=mid
tails[left]=num
if left==res:
res+=1
return res
5. 补充
二分查找代码:
def binarySearch(A,left,right,x): #在A的[left,right]区间查找x
while left<=right:
mid=(left+right)//2
if A[mid]==x:
return mid
elif A[mid]>x:
right=mid-1
else:
left=mid+1
return -1
找到数组中第一个大于等于x的元素的idex:
def lower_bound(A,left,right,x): #在A的[left,right]区间查找x
while left<right:
mid=(left+right)//2
if A[mid]>=x:
right=mid
else:
left=mid+1
return left
找到数组中第一个大于x的元素的idex:
def upper_bound(A,left,right,x): #在A的[left,right]区间查找x
while left<right:
mid=(left+right)//2
if A[mid]>x:
right=mid
else:
left=mid+1
return left