题目链接:300. 最长递增子序列 - 力扣(LeetCode)
给你一个整数数组 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))吗?
方法一:动态规划
从一般情况考虑,求dp[i](设dp[i]表示以nums[i]结尾的最长递增子序列长度),只需要找出nums[i]之前比其小的nums[j],那么dp[i]就等于dp[j] + 1
由于要求最大,所以在遍历的时候用max更新最大的即可
递推公式已经出来了,接着考虑初始化。显然每个元素都初始化为1,因为一个数字也可以构成一个子序列
最后,用max更新最大的dp[i]即可
Java
java
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
int ans = dp[0];
for(int i = 1; i < n; i++) {
for(int j = i - 1; j >= 0; j--) {
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
C++
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int ans=0;
int n=nums.size();
if (n<=1) return n;
vector<int>dp(n,1);
for(int i=1;i<n;i++) {
for(int j=0;j<i;j++) {
if(nums[j]<nums[i]) {
dp[i]=max(dp[i],dp[j]+1);//这里不是比较,而是更新最大值
}
}
ans=max(ans,dp[i]);//这里同理,也是在更新最大值
}
return ans;
}
};
Go
Go
func lengthOfLIS(nums []int) int {
dp:=make([]int,len(nums))
for i:= range dp {
dp[i]=1
}
ans:=dp[0]
for i:=1;i<len(nums);i++ {
for j:=0;j<i;j++ {
if nums[j]<nums[i] {
dp[i]=max(dp[i],dp[j]+1)
}
}
ans=max(ans,dp[i])
}
return ans
}
时间复杂度为O(N^2),考虑进行优化
方法二:贪心+二分
假设nums为[1,2,99999,3,4],当遍历完前两个元素,也就是1和2之后,此时最长递增子数组(以下简称LIS)为[1,2]
接下来你会选择99999加入LIS中,还是选择后面的3加入到LIS中?
显然应该选择更小的3,如果选择99999,那么就会错过3后面的4
选择了3,就能得到正确的LIS,即[1,2,3,4]
这是一种贪心思想:为了使数组递增的长度更大,我们应该让它增长的速度尽可能慢,这样就尽可能避免了错过后面单增的可能,表示一种"最可能性"
落实到具体数组中,我们的选择策略尽管依然是选择比LIS中元素更大的元素,但是新增了一个条件是:要在这些更大的元素中,选择最小的那个。但问题来了,我们在遍历的时候,怎么知道后面的元素是否有更小的?
我们可以记录当前LIS的长度len,用一个数组d记录LIS的末尾元素(也就是最大值)的最小值,即d[len]表示长度为len的LIS的末尾元素的最小值
显然d[i]随i的增大而增大。因为如果 d[j]≥d[i] 且 j<i,考虑从长度为i的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 d[i](因为第j个元素不是最后一个元素,就必然比最后一个元素要小),也就小于 d[j],那么就找到了一个长度为 j 的最长上升子序列,并且末尾元素比d[j]小,而d[j]的定义是长度为j的LIS的末尾元素的最小值,这与定义相矛盾,因此单调性得证
如果遍历的元素nums[i]比d[len]更小,说明存在一种"增长速度更慢"的可能,是哪个子数组的增长速度可能更慢?为了回答这个问题,我们需要在d中找到从右往左数第一个比nums[i]更小的元素,由于d有序,可以用二分。假设其位置为k,那么将d[k+1]替换为nums[i]后,d[1...k+1]的增长速度就比没替换之前更慢,长度为k+1的LIS后续变得更长的可能性就变高了
如果nums[i]比d[len]更大,那就意味着nums[i]可以成为长度为len+1的LIS的末尾元素。直接让len自增1,并且让d[len+1]为nums[i]即可,因为此时len的长度发生了变化,只有一个长度为len+1的LIS,所以末尾元素的最小值只能是新加入的nums[i]了
Java
java
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if(n == 0) return 0;
int[] d = new int[n + 1]; //d[len]表示长度为len的LIS末尾元素的最小值
int len = 1; //LIS的长度,初始时只有第一个元素,因此为1
d[len] = nums[0]; //初始时,len为1,LIS为第一个元素自己构成的数组,所以最小值就是它本身
for(int i = 1; i < n; i++) {
if(nums[i] > d[len]) {
//长度为len的、末尾元素最小的LIS的最大值遇到了比它更大的元素,应该将其加入该LIS当中
d[++len] = nums[i]; //原本长度为len,现在长度为len+1,d[len+1]表示的是长度为len+1的LIS末尾元素的最小值
} else {
//二分法找到d中从右往左数第一个比nums[i]小的位置pos,让d[pos+1]为nums[i]
//如果d中所有元素都比nums[i]要大,更新d[1],因为以nums[i]结尾的、长度为1的LIS有最小的末尾元素,所以pos初始值为0
int pos = 0;
int l = 1, r = len; //d[0]没有实际含义,所以左端点为1,右端点为len
while(l <= r) {
int mid = (l + r) >> 1;
if(d[mid] < nums[i]) {
pos = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
d[pos + 1] = nums[i];
}
}
return len;
}
}