解法一:自底向上的递推DP
我们设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 dp[0...i-1] 都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]?
定义dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
根据这个定义,我们就可以推出 base case:dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列起码要包含它自己。最终答案(递增子序列的最大长度)应该是整个 dp 数组中的最大值。
接下来如何确定状态转移方程?
例如,现在想求 dp[5] 的值,也就是想求以 nums[5] 为结尾的最长递增子序列。
nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3小的递增子序列,然后把 3接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度+1。
而题目要求最长的递增子序列,那么,nums[0] 和 nums[4] 都是小于 nums[5] 的,然后对比 dp[0] 和 dp[4] 的值,我们让 nums[5] 和更长的递增子序列结合。
func lengthOfLIS(nums []int) int {
// dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
dp := make([]int, len(nums))
for i:= 0; i<len(dp); i++{ // 初始化都为1,因为至少包含当前位置上的元素
dp[i] = 1
}
for i:= 0; i<len(dp); i++{
for j:=0; j<i; j++{ // 寻找 nums[0..i-1] 中比 nums[i] 小的元素
if nums[j] < nums[i]{
// 把 nums[i] 接在后面,即可形成长度为 dp[j] + 1的递增子序列
dp[i] = max(dp[i], dp[j]+1)
}
}
}
res := 0
for _, v := range dp{
res = max(res, v)
}
return res
}
func max(a, b int) int{
if a > b{
return a
}
return b
}
时间复杂度:O(N^2),遍历计算 dp 列表需 O(N),计算每个 dp[i] 需 O(N),显然不是最优解
空间复杂度 O(N): dp 列表占用线性大小额外空间
优化版:二分查找
参考 labuladong.online/algo/dynami…
func lengthOfLIS(nums []int) int {
// 初始所有牌是平铺的,没有牌盖在上面
// top[i]表示长度为i+1的子序列尾部元素的值,如果有多个子序列长度一样,这里存的是最小的那个序列尾部元素
top := make([]int, len(nums))
// 牌堆数初始化为 0
var piles int
for i := 0; i < len(nums); i++ {
// 要处理的扑克牌
poker := nums[i]
// ***** 在所有堆上搜索左侧边界的二分查找 *****
var left, right int = 0, piles
for left < right {
mid := (left + right) / 2
if top[mid] > poker {
right = mid
} else if top[mid] < poker {
left = mid + 1
} else if top[mid] == poker{
right = mid
}
}
// ********************************
// 没找到合适的牌堆,新建一堆
if left == piles {
piles++
}
// 把这张牌放到牌堆顶
top[left] = poker
}
fmt.Println(top)
// 牌堆数就是 LIS 长度
return piles
}
时间复杂度:O(N*Log(N))
空间复杂度 O(N)