前言
网上包括ACWing上有很多关于本题的讲解,但是都不是很好理解,讲的大都不太清晰。为此,自己也是捣鼓蛮久,专门写一篇博客来进行巩固和讲解。
题目
题目链接:896. 最长上升子序列 II
给定一个长度为 N N N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数
N
N
N。
第二行包含
N
N
N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1
≤
N
≤
100000
,
1≤N≤100000,
1≤N≤100000,
−
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列表内的元素是单调递增的。(二分只有单调时才可以用)
相信大家都不喜欢看文字,这里我画个图来举个例子。
- 初始值a:[3,1,2,1,8,5,6],q[负无穷,0,0,0,0,0,0]
- len = 0, q[len] = a[0]
- 比较q[len] 与 a[1],因为a[1] < q[len],所以找到第一个 大于等于a[1]的数进行替换,也就是把q[1] 替换为 a[1]
- a[2] 与 q[len] 进行比较, 因为 a[2] > q[len] ,所以 len += 1, q[len] = 2
- a[3] < q[len], 所以找第一个大于等于a[3] 的数进行替换
- 之后的操作跟上述类似,之后直接给出每次操作后的结果
- 最后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)
总结
我的代码可能跟别人比起来不是那么简洁,但是我个人认为还是很好理解的,个人偏向于好理解和好写的代码,而非很简洁的代码。