最长上升子序列 II(包能看懂的版本,小白必看,贪心,python讲解,其他语言均适用)

前言

   网上包括ACWing上有很多关于本题的讲解,但是都不是很好理解,讲的大都不太清晰。为此,自己也是捣鼓蛮久,专门写一篇博客来进行巩固和讲解。

题目

题目链接:896. 最长上升子序列 II

给定一个长度为 N N N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数 N N N
第二行包含 N N N 个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1 ≤ N ≤ 100000 , 1≤N≤100000, 1N100000
− 1 0 9 ≤ 数列中的数 ≤ 1 0 9 −10^9≤数列中的数≤10^9 109数列中的数109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

思路

   本题不同于之前的最长上升子序列 I,最长上升子序列 I 里使用的算法是动态规划,时间复杂度是 O ( n 2 ) O(n^2) O(n2)。而本题的 n n n 1 0 5 10^5 105 n 2 n^2 n2则为 1 0 10 > 1 0 8 10^{10} > 10 ^8 1010>108,显然超时。而想要不超时则需把时间复杂度控制在 O ( n l o g n ) O(nlogn) O(nlogn)以内,本题采用的算法则是二分查找(为什么使用二分后面再说),策略是贪心策略。

   因为题目求的是最长上升子序列的长度而不是序列(动态规划中可以求出具体的序列是多少,贪心则不行),那么我们可以这样来思考本题:

   首先定义一个空列表q,把a列表的第一个元素放到q中。然后依次取出列表a中元素的值,把它与q中末尾元素进行比较,如果大于q的末尾元素,则把该元素放入q中,否则(小于q中最后一个元素),找到q列表中第一个大于或者等于该元素的值并进行替换!!!, 列表a中每个元素都进行如此操作,最后列表q中元素的个数就是最长上升子序列的长度。

   其中找到q列表中第一个大于或者等于该元素的值并进行替换,这一步操作就可以用二分查找来进行,因为按如上要求修改q列表时,q列表内的元素是单调递增的。(二分只有单调时才可以用)

   相信大家都不喜欢看文字,这里我画个图来举个例子。

  1. 初始值a:[3,1,2,1,8,5,6],q[负无穷,0,0,0,0,0,0]
    在这里插入图片描述
  2. len = 0, q[len] = a[0]

在这里插入图片描述

  1. 比较q[len] 与 a[1],因为a[1] < q[len],所以找到第一个 大于等于a[1]的数进行替换,也就是把q[1] 替换为 a[1]
    在这里插入图片描述
  2. a[2] 与 q[len] 进行比较, 因为 a[2] > q[len] ,所以 len += 1, q[len] = 2

在这里插入图片描述

  1. a[3] < q[len], 所以找第一个大于等于a[3] 的数进行替换

在这里插入图片描述

  1. 之后的操作跟上述类似,之后直接给出每次操作后的结果

在这里插入图片描述

在这里插入图片描述

  1. 最后q 列表如下图所示,len的值此时为4.
    在这里插入图片描述

   结合图片来看,本题的解题思路就是这样。每次选取的元素如果大于q中的末尾元素,则添加,否则进行替换,替换的过程中总长度不会发生改变(但不能得知最后最长上升子序列是什么了)

如何进行二分查找呢?

   之前说了这么多了,但依旧没有讲到是怎么二分的。其实贪心的思路知道了,二分就很好操作了,每次令 l = 0, r = len ,然后找到第一个大于等于a[i]的数的下标即可,q[0] 设置为负无穷的作用在这里就可以体现出来了, 因为数列中的数最小可以是 − 1 0 9 -10^9 109,所以我们干脆设置为负无穷防止之后计算出现差错。

二分代码:

        l, r = 0, len
        while l < r:
            mid = (l + r) // 2
            if q[mid] >= a[i]:
                r = mid 
            else:
                l = mid + 1
        q[l] = a[i]

完整代码及注释

# 读取输入的整数 n,表示数组的长度
n = int(input())

# 读取一行输入,将其按空格分割成多个字符串,
# 再将每个字符串转换为整数,最后存储在列表 a 中
a = list(map(int, input().split()))

# 初始化一个长度为 n + 1 的列表 q,用于存储上升子序列的末尾元素
# 这里长度为 n + 1 是为了方便后续操作,避免越界问题
q = [0] * (n + 1)

# 将 q[0] 设置为负无穷大,作为一个边界条件,方便后续二分查找
q[0] = float("-inf")

# 初始化 q[1] 为数组 a 的第一个元素,因为初始时最长上升子序列长度为 1
q[1] = a[0]

# 初始化最长上升子序列的长度为 1
len = 1

# 从数组 a 的第二个元素开始遍历
for i in range(1, n):
    # 如果当前元素 a[i] 大于 q 数组中当前最长上升子序列的末尾元素
    if a[i] > q[len]:
    	# 最长上升子序列的长度加 1
    	len += 1
        # 则将 a[i] 添加到 q 数组的末尾,作为新的最长上升子序列的末尾元素
        q[len] = a[i]
        
        
    else:
        # 如果当前元素 a[i] 不大于 q 数组中当前最长上升子序列的末尾元素
        # 则需要在 q 数组中找到第一个大于等于 a[i] 的位置
        # 初始化二分查找的左右边界
        l, r = 0, len
        # 开始二分查找
        while l < r:
            # 计算中间位置
            mid = (l + r) // 2
            # 如果 q[mid] 大于等于 a[i],说明第一个大于等于 a[i] 的位置在左半部分
            if q[mid] >= a[i]:
                # 更新右边界为 mid
                r = mid 
            else:
                # 否则,说明第一个大于等于 a[i] 的位置在右半部分
                # 更新左边界为 mid + 1
                l = mid + 1
        # 找到位置后,将 a[i] 替换该位置的元素
        q[l] = a[i]

# 输出最长上升子序列的长度
print(len)

总结

   我的代码可能跟别人比起来不是那么简洁,但是我个人认为还是很好理解的,个人偏向于好理解和好写的代码,而非很简洁的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不染_是非

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值