1. 问题描述:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-increasing-subsequence/
2. 思路分析:
① 最长上升子序列是动态规划中一类比较经典的题目,力扣的官方提供了两种思路,第一种思路是以当前数字 nums[i] 结尾的最长递增子序列的长度,第二种思路是贪心 + 二分查找的策略,时间复杂度分别是 O(n ^ 2) 与 O(nlogn),下面是我对于这两种思路的理解。
② 第一种思路使用一维的dp数组可以解决,dp 数组的含义表示的是以当前位置i对应的数字结尾的最长递增子序列的长度,我们可以根据 [0:j] 这些 dp 数组的值递推出当前位置 i 对应的 dp[i] 的值,尝试将当前的 nums[i] 的值放在 [0:j](j < i)位置的后面判断一下是否能够构成更长的递增子序列,如果更长那么更新 dp[i] 的值,这样我们就可以根据之前 dp 数组的值递推出当前位置的值,最终我们只需要求解出 dp 数组中的最大值即可。
③ 第二种思路感觉是比较难想出来的:贪心 + 二分查找的策略,要想构成更长的递增子序列那么我们尽量使得构成序列的数字尽可能小,这样构成的序列的长度才尽可能大(贪心),这里也使用到一个 dp 数组,dp[i] 表示的是长度为i结尾的最小的数字,并且 dp 数组中存放的数字为递增的序列,初始化 dp[1] = nums[0],我们可以从前往后遍历数组,当发现当前的数字比 dp[l] 大那么将nums[i]添加到dp数组的后面,当发现比 dp[l] 小那么我们需要在 [0:l] 中找到插入的位置将 nums[i] 插入到这个位置中这样的话 dp 数组中保存的都是序列比较小的值,最后返回这个长度 l 即可,因为 dp 数组是有序的所以我们可以使用二分查找的方法查找插入的位置。
可以根据具体的例子 + debug 调试会更容易理解一点,参考例子:[9, 6, 2, 3, 7]
3. 代码如下:
dp[i]表示以当前数字nums[i]结尾的最长递增子序列的长度:
from typing import List
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums: return 0
n = len(nums)
dp = [0] * n
for i in range(n):
dp[i] = 1
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
dp[i]表示以当前长度i结尾的最小的数字:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
d = []
for n in nums:
if not d or n > d[-1]:
d.append(n)
else:
l, r = 0, len(d) - 1
loc = r
while l <= r:
mid = (l + r) // 2
if d[mid] >= n:
loc = mid
r = mid - 1
else:
l = mid + 1
d[loc] = n
return len(d)
go:
package main
import (
"fmt"
)
func lengthOfLIS(nums []int) int {
n := len(nums)
var f []int
for i := 0; i < n; i++ {
x := nums[i]
if len(f) == 0 || f[len(f)-1] < x {
f = append(f, x)
} else {
l, r := 0, len(f)-1
for l < r {
mid := (l + r) >> 1
if f[mid] >= x {
r = mid
} else {
l = mid + 1
}
}
f[r] = x
}
}
return len(f)
}