300. 最长递增子序列

题目描述

思路
解决这道题有两种思路,一种是动态规划,一种是二分查找,我们分别用两种思路来解决这道题目。
思路 1:动态规划
首先,这道题目可以使用动态规划来解决。这道题使用动态规划的方法比较有意思,我们维护一个数组dp用来记录从下标0开始到位置i这个区间内的最长递增子序列的长度是多少。具体来说,dp[i] = 4指的就是0...i这个区间内,最长递增子序列的长度为4。那么我们应该如何维护dp数组呢?答案是通过一个双层循环来进行维护。
具体来说,我们在最外层循环使用i来遍历整个数组,在第二层循环使用j来遍历0...i。如果满足nums[i] > nums[j],就说明nums[j], nums[i]构成一个单调递增序列,这个序列的长度至少为2(答案至少是1,因为单个元素本身就是长度为1的单调子序列)。由此我们可以得到状态转移方程,dp[i] = max(dp[j] + 1, dp[i])。由于我们是从前往后顺序对nums数组进行遍历的,因此在遍历的过程中会完成对dp数组的维护。
最终我们需要得知最长递增子序列,也就是dp数组当中的最大值,直接使用 Golang 的库函数slices.Max(dp)即可。
思路 2:二分查找
原题目描述当中提到,请使用O(nlogn)O(n\log{n})O(nlogn)的算法来完成进阶做法,实际上这道题目的最优解是使用二分查找。需要注意的是,我们只能对明确具有单调性质的序列使用二分查找算法,显然原题当中给出的数组并不是一个具有单调性的数组,那么我们如何使用二分查找呢?
实际上,我们需要维护一个额外的数组,它记录的就是当前子序列0...i的最长递增子序列,我们将其命名为tails,并初始化其长度为n(n := len(nums))。我们对nums数组进行遍历,期间我们同时维护两个信息,分别是tails数组以及其右边界的尾后下标。针对每个nums[i],我们需要在区间0...res当中进行一次二分查找,来找到放置nums[i]到tails数组(当前最长上升子序列)当中的位置。当我们找到这个位置之后,令tails[l] = nums[i],并判断r和res的关系:如果r == res,就说明右边界没有移动,新放置到tails当中的元素在res这个位置上,此时需要令res += 1;否则,说明tails[0..<res]的某个位置被修改了,此时不需要移动res。
举个简单的例子,针对序列[7, 8, 9, 1, 2, 3],我们肉眼可以观察到最长上升子序列的长度就是3。对应到该算法当中,我们初始化了一个长度为6的序列tails,初始时res的位置是0。接下来我们开始对nums进行遍历,每次遍历时令l, r := 0, res。显然在第一次遍历时,l == r(它们都是0),我们放置tails[l] = nums[i],此时的tails = [7, 0, 0, 0, 0, 0],由于res == r,它们都是0,所以令res += 1。
接下来,nums[1] == 8,初始时l, r := 0, 1,通过二分查找,最终的l == 1,因此将nums[1]的元素8放置到tails[1]这个位置。由于此时仍然有r == res,因此res += 1。
然后,nums[2] == 9,和方才的情况相同,tails[3] == 9,res += 1。
从 nums[3] == 1开始,情况开始不同。此时l, r := 0, 3,通过二分查找,放置nums[3] == 1的位置就是tails[0],此时tails[0..<res]的子序列变为了[1, 8, 9],最长上升子序列的长度仍然是3,只不过序列的值变化了。由于右指针r向左移动了,不满足r == res,因此res不必移动。
以此类推,最终tails[0..<res]的值其实是[1, 2, 3],长度就是res的值。
由此,最终的答案就是res。
Golang 题解
思路 1:动态规划
go
func lengthOfLIS(nums []int) int {
n := len(nums)
dp := make([]int, n)
for i := 0; i < n; i ++ {
dp[i] = 1
for j := 0; j < i; j ++ {
if nums[j] < nums[i] {
dp[i] = max(dp[i], dp[j] + 1)
}
}
}
return slices.Max(dp)
}
思路 2:二分查找
go
func lengthOfLIS(nums []int) int {
n := len(nums)
tails, res := make([]int, n), 0
for i := 0; i < n; i ++ {
l, r := 0, res
for l < r {
mid := (l + r) / 2
if tails[mid] < nums[i] {
l = mid + 1
} else {
r = mid
}
}
tails[l] = nums[i]
if r == res {
res += 1
}
}
return res
}