刷题使我快乐,满脸开心.jpg
谢谢大家的支持!点个在看再走哟~
- 来源:力扣(LeetCode)
- 链接:https://leetcode.cn/problems/longest-increasing-subsequence/
- 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目
给你一个整数数组 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))
吗?
思路
这道题应该是比较容易想到动规方法的,只是常规的状态方程会是O(n*n)
复杂度,不过也很有意义,所以下面代码里也放出来了。只是进阶既然都写出来了,那就得挑战下试试啦~
暂时我其实没有除了动规方法之外的进阶方法,所以这里只分析下是如何想到贪心+二心思路的,权当参考:
1、首先,我的思路就是改良动规方法,动规方法里能看得出来会有一个双层循环来更新dp数组,那如何能够降低这里的复杂度呢?当时是感觉有两种方式
- 要么是改状态方程,使得dp数组可以保证有序,用二分法降低复杂度;
- 要么是不改状态方程,但是找到大范围剪枝的方法,从而降低复杂度
2、当时想了下,在现有状态方程下,很难找到剪枝方法,所以主要思考方向就放在了改状态方程上。
那问题就是,如何改能保证有序?
可以试着提一个贪心
的假设:
如果我找到一个子序列,它在任意一个位置上的值
child[i]
,都不大于i+1
长度的子序列在末尾位置上的值,那么我们就能够找到一个最长的递增子序列。(详细推论可以参考leetCode上的题解,我这里其实也是先想到思路,然后发现这个思路可行的,结果论证过程看了好久(捂脸。。。)
那状态方程有了,开始编码,具体思路和解释会在代码注释上给出
代码
常规dp,O(n*n)
func lengthOfLIS(nums []int) int {
if len(nums) <= 1 {
return len(nums)
}
// dp数组含义为dp[i]表示从0到i长度的数组中,递增子序列的最大长度
dp := make([]int, len(nums))
n := len(nums) - 1
dp[0] = 1
maxLen := 1
for i := 1; i <= n; i++ {
dp[i]=1
for j := 0; j < i; j++ {
if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j]+1)
}
}
maxLen = max(maxLen, dp[i])
}
return maxLen
}
贪心dp,O(n*log(n))
func lengthOfLIS(nums []int) int {
// dp数组含义为dp[i]表示i长度的递增子序列中,最小的末尾值。此状态定义,也决定了dp数组有序。本质是一个贪心的思路,论证可以参考题解
dp := make([]int, len(nums))
// 当前dp数组的最大长度,也就是最长子序列长度
dpLen := 0
for _, num := range nums {
i, j := 0, dpLen
for i < j {
m := (i + j) / 2
// 因dp数组在此状态设定下有序,二分找比num小的数
if dp[m] < num {
i = m + 1
} else {
j = m
}
}
// 退出时,i==j。此时有两种情况,一个是num比现有dp数组中某值小,那就更新dp数组
// 注意例如更新dp[2]意味着在长度为3的子序列中,位置3的值最小可以为此值,
// 并非指最长子序列中,位置3的值为此值,
// 所以此dp方程只是想根据这个设定实现dp本身递增而优化查询
dp[i] = num
// 另一个是num比现有dp数组中值都大,那就更新扩展了dp数组长度,所以需要将最大长度更新
// 当然同时,这个状态下,此是dp数组中的状态又保证了,'确实有这个长度的子序列存在'
if j == dpLen {
dpLen++
}
}
return dpLen
}
看到这里了,点个赞点个在看呗!~ 多谢啦!~