300. 最长递增子序列
300. 最长递增子序列 (Medium)
给你一个整数数组 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
-10⁴ <= nums[i] <= 10⁴
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
分析
动态规划
最容易想到的做法:
func lengthOfLIS(nums []int) int {
n := len(nums)
dp := make([]int, n)
res := math.MinInt32
for j, v := range nums {
dp[j] = 1
for i, u := range nums[:j] {
if u < v && dp[i]+1 > dp[j] {
dp[j] = dp[i] + 1
}
}
res = max(res, dp[j])
}
return res
}
时间复杂度 O(n^2),空间复杂度 O(n)。
基于该解法,也能容易地得到这个子序列:只需记住得到最长递增子序列的位置,然后向前遍历,依次得到前一个元素:
func lengthOfLIS(nums []int) int {
n := len(nums)
dp := make([]int, n)
res := math.MinInt32
mi := 0 // 记录最长上升子序列的结束位置
for j, v := range nums {
dp[j] = 1
for i, u := range nums[:j] {
if u < v && dp[i]+1 > dp[j] {
dp[j] = dp[i] + 1
}
}
if dp[j] > res {
res = dp[j]
mi = j // 更新 mi
}
res = max(res, dp[j])
}
// 根据 mi 获取最终结果
lis := make([]int, res)
j := len(lis)-1
lis[j] = nums[mi]
pre := mi
j--
for i := mi-1; i >= 0 && j >= 0; i-- {
if nums[i] < nums[pre] && dp[i] == dp[pre]-1 {
lis[j] = nums[i]
j--
pre = i
}
}
fmt.Println(lis)
return res
}
贪心
这是今天的主角。尝试贪心地构建结果:
如果要使上升子序列尽可能长,则需要让序列上升得尽可能慢,因此在构建结果的时候,每次在上升子序列最后加上的那个数需要尽可能小。 建立 memo 数组,memo[i]代表长度为 i+1 的递增子序列末尾数字 遍历 nums,对于当前元素: 如果大于结果数组最后元素,直接追加到结果数组最后; 否则,在结果数组里找到第一个不小于当前元素的元素,并将其更新为当前元素。 这里可以用二分法降低复杂度。
以 [2,1,5,3,4,8,9,7] 为例,可以得到 memo 数组为 [1,3,4,7,9],这表示: 长度为 1 的递增子序列,最佳末尾数字是 1 长度为 2 的递增子序列,最佳末尾数字是 3 长度为 3 的递增子序列,最佳末尾数字是 4 长度为 4 的递增子序列,最佳末尾数字是 7 长度为 5 的递增子序列,最佳末尾数字是 9
可见,memo 数组的长度就是最长递增子序列的长度。
实际上,以上做法是一个不完全的耐心排序(patience sorting)。没有完全排序所有元素,而是借助耐心排序的第一部分,得到了最长递增子序列的长度。
func lengthOfLIS(nums []int) int {
memo := make([]int, len(nums))
length := 0
for _, v := range nums {
j := sort.SearchInts(memo[:length], v)
memo[j] = v
if j == length {
length++
}
}
return length
}