最长上升子序列【动态规划,二分查找】

题目描述(leetcode-300)

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

解法一 动态规划

思路:dp[i]表示直到第i个元素的最长上升子序列的长度,初始条件dp[i]=1dp[i]如何更新呢?若第i个值大于其前面某一个元素(假设索引为j),那么dp[i]=dp[j]+1,故可令j0~i-1索引,用一个max_val记录当前dp[i]的最大值,一旦某个值小于nums[i],则更新max_val。即:dp[i]=max(dp[j])+1

图解。

代码
def maxLIS(nums):
    length = len(nums)
    if length < 1:
    	return 0
    # 返回结果
    max_ans = 1
    dp = [1 for i in range(length)]
    for i in range(1,length):
        # 注意,这里的max_val要是0,因为后面dp[i] = max_val + 1,若这里不是0,当下面的for循环没有找到比nums[i]小的数的时候
        # dp[i]本来是1,这样就变成2了。
    	max_val = 0
    	# 遍历小于i的dp[j]
    	for j in range(i):
    	    if nums[i] > nums[j]:
    	    	max_val = max(max_val,dp[j])
    	dp[i] = max_val + 1
    	# 更新max_ans
    	max_ans = max(max_ans,dp[i])
    return max_ans	
复杂度分析

空间复杂度 O ( N ) O(N) O(N),时间复杂度 O ( N 2 ) O(N^2) O(N2)

解法二 动态规划+二分搜索

思路:把dp当作到现在元素为止的最大上升子序列,那么当来了一个新元素后,如果这个元素的值大于dp最后一个元素,则直接将其添加到dp尾部,若小于等于,则用二分搜索的方法找出它该放的位置。这个位置是第一个大于或等于它的数的位置,并替代原来的数。这样虽然得到的dp有可能不再是原先数组的子序列,但是递增子序列的长度没有改变。

代码
def maxLIS(nums):
	length = len(nums)
	if length<1:
		return 0
	# 将第一个元素放入dp中
	dp = [nums[0]]
	dp_len = 1
	for i in range(1,length):
		# 若当前值大于dp中最后一个值,则直接将其添加至dp中
		if nums[i]>dp[-1]:
			dp.append(nums[i])
			dp_len += 1
		else:
		# 否则,二分查找当前数该放的位置
			left = 0
			right = dp_len - 1
			while(left<right):
				mid = left + (right - left) // 2
				if dp[mid] < nums[i]:
					left = mid + 1
				else:
					right = mid
			dp[left] = nums[i]
	return dp_len
复杂度分析

空间复杂度 O ( N ) O(N) O(N),时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

变形一 求最长上升子序列的数目(leetcode-673)

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

先上代码,再解释

def findNumberOfLIS(self, nums: List[int]) -> int:
        length = len(nums)
        if length < 1:
            return 0
        # 返回结果
        max_ans = 1
        dp = [1 for i in range(length)]
        dp_num = [1 for i in range(length)]
        for i in range(1,length):
            # 遍历小于i的dp[j]
            for j in range(i):
                # 如果第j个数小于第i个数
                if nums[i] > nums[j]:
                    # 如果dp[i]小于dp[j]+1,那么表示可通过添加nums[i]构成一个更长的LIS,此时只需将LIS的数目复制即可。
                    if dp[i] < dp[j] + 1:
                        dp[i] = dp[j] + 1
                        max_ans = max(max_ans,dp[i])
                        dp_num[i] = dp_num[j]
                    # 如果dp[i] == dp[j] + 1,那么说明添加nums[i]后构成的LIS的长度,在之前出现过,那么需要把之前的数目加在此处
                    elif dp[i] == dp[j] + 1:
                        dp_num[i] += dp_num[j]
        # 遍历所有长度最长的LIS,将数目累加起来即可。
        cnt = 0
        for i in range(length):
            if dp[i] == max_ans:
                cnt += dp_num[i]
        return cnt

这个题目的解法和上个题目的解法一类似,只是在解法一中新增了一个记录当前为止在当前长度下LIS的数目的数组dp_num
同样,遍历小于inums[j],当nums[i]>nums[j]时考虑两种情况:

  1. dp[i] < dp[j] + 1,说明可通过添加nums[i]构造一个更长的LIS(LIS的数目不变),那么dp_num[i]直接赋值为dp_num[j]即可;
  2. dp[i] == dp[j]+1,说明dp[i]这个长度的LIS在之前出现过,那么dp_num[i]应该加上dp_num[j]的值。

看一个例子:nums=[2,5,3,7]
初始化:dp=[1,1,1,1]dp_num=[1,1,1,1]
step1:i = 1,j=0,此时dp[i] < dp[j]+1,故dp[i]=dp[j]+1,dp_num[i]=dp_num[j],此时dp=[1,2,1,1]dp_num=[1,1,1,1]
step2:i = 2,j=0,此时dp[i] < dp[j]+1,故dp[i]=dp[j]+1,dp_num[i]=dp_num[j],此时dp=[1,2,2,1]dp_num=[1,1,1,1]
step3:i = 2,j=1,此时nums[i]<nums[j],不作处理
step4:i = 3,j=0,此时dp[i] < dp[j]+1,故dp[i]=dp[j]+1,dp_num[i]=dp_num[j],此时dp=[1,2,2,2]dp_num=[1,1,1,1]
step5:i = 3,j=1,此时dp[i] < dp[j]+1,故dp[i]=dp[j]+1,dp_num[i]=dp_num[j],此时dp=[1,2,2,3]dp_num=[1,1,1,1]
step6:i = 3,j=2,此时dp[i] == dp[j]+1,故dp_num[i]+=dp_num[j],此时dp=[1,2,2,3]dp_num=[1,1,1,2],表示长度为3的LIS共有两个。
在更新dp[i]时记录最大值,然后遍历dp,当dp[i]等于这个最大值时,将对应的dp_num[i]加入结果即可。

变形二 获得最大上升子序列

思路:此处需要记录这个最长的子序列,大体方法同方法一
新增一个列表LIS,用于记录每个位置的LIS
对于LIS[i],若nums[i]>nums[j],其为max_length(LIS[j]).append(nums[i]) j=0…i-1,即LIS[j]中最长的加上当前元素
则使用一个变量记录前面i-1个LIS中长度最大的LIS的索引,然后LIS[i] = LIS[max_index]+[nums[i]]
如果这个max_index没有更新过,说明所有的nums[j]都大于nums[i],那么LIS[i]=[nums[i]]

def LIS_N2(nums):
    length = len(nums)
    if length<2:
        return length
    dp = [1 for i in range(length)]
    LIS = [[nums[0]]]
    ans = 1
    for i in range(1,length):
        max_val = 0
        max_index = -1
        for j in range(i):
            if nums[i]>nums[j]:
                if dp[j] > max_val:
                    max_index = j
                    max_val = dp[j]
        # 若max_index不为-1,说明其更新过
        if max_index != -1:
            LIS.append(LIS[j]+[nums[i]])
        else:
            LIS.append([nums[i]])
        dp[i] = max_val + 1
        ans = max(ans,dp[i])
    
    # 然后搜索LIS,把最长的取出即可
    return ans
  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值