大家好!我是曾续缘💖
今天是《LeetCode 热题 100》系列
发车第 87 天
动态规划第 7 题
❤️点赞 👍 收藏 ⭐再看,养成习惯
最长递增子序列 给你一个整数数组
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 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
难度:💖💖
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
解题方法
定义f[i]
为:长度为k+1
的子序列的末尾元素。下标代表了长度,值代表的是原数组中的一个值,作为子序列的末尾元素。
初始化:
f
数组的长度为0,很好理解吧,不存在下标,即不存在任何长度的子序列。
遍历数组进行状态转移:
-
因为我们是一个数一个数地遍历的,对于当前遍历的数
num
,尝试把它加到目前最长子序列的末尾。- 目前最长子序列的末尾在哪里呢?根据
f
的定义,最长子序列的长度就是f
最大的下标加1
,最长子序列的末尾元素就是f[最大的下标]
- 目前遍历的数
num
能不能加到目前最长子序列的末尾呢?如果num
大于目前最长子序列的末尾元素f[最大的下标]
,说明递增,可以加入。 - 加入之后会导致什么呢?最长子序列的长度+1了,末尾元素也改变了,变成了
num
。 - 最长子序列的长度变化了,怎么办呢,怎么改呢?根据
f
的定义,f
的最大下标要加1了,说明最长子序列长度+1了,同时f[新最大下标]
等于num
了,因为最长子序列的尾部是当前遍历的num
了。 f
数组的最大下标+1了,也填入值了,原来的f
最大下标的值需要改吗?不需要,因为这个下标对应的子序列长度没变化啊,我们也不愿意将num
作为它的尾部,因为num
更大,不利于贪心。
- 目前最长子序列的末尾在哪里呢?根据
-
如果
num
不能加到目前最长子序列的末尾,说明最长子序列的长度不会变化。- 那我要
num
有何用?我们可以用num
来替换掉其他长度的子序列的尾部啊,让它们变小点,方便后面新增的数能更小的拼接上来。 - 好,我们从头遍历
f
数组,不断尝试更新f[i]
的尾部元素,和num
取min,越小越好。 - 但是这样算法时间复杂度就变成了 O ( n 2 ) O(n^2) O(n2)。
- 想想我们有必要更新所有的
f
值吗?比如说长度为1,2,3的子序列的尾部,都能更新为更小的num
。我们更新了3,还有必要更新1,2吗?更新了也不会导致子序列的长度变长,就算后面有数拼接上来,我们也是选择拼接在长度为3的子序列上啊,因为贪它更长啊。 - 所以我们选择更新最后一个能更新的,之前的不用考虑了。
- 如何找到最后一个能更新的位置呢?这显然和我们的
lowerbound
方法不谋而合。使用二分的方法,以 O ( l o g n ) O(logn) O(logn)的时间复杂度迅速找到最后一个能将尾部更新为num
的f
数组下标,更新之。- 为什么能用二分呢?eeee因为
f
数组是递增的。
- 为什么能用二分呢?eeee因为
- 那我要
返回值:根据f
的定义,最长子序列的长度就是f
的最大下标+1,即f
数组的长度。
Code
class Solution {
public int lengthOfLIS(int[] nums) {
List<Integer> f = new ArrayList<>();
for (int num : nums) {
if (f.isEmpty() || num > f.get(f.size() - 1)) {
f.add(num);
} else {
int l = lowerbound(f, num);
f.set(l, num);
}
}
return f.size();
}
public int lowerbound(List<Integer> f, int num) {
int l = 0, r = f.size() - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (f.get(mid) >= num) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return l;
}
}