300. 最长递增子序列
给你一个整数数组 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(n2) 的解决方案吗?
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
题解①:
- 由于此类型题目序列是可以随便组合的(即只要大小满足递增即可),而这种组合的方式有许多许多,如果只采用循环遍历发现难以解决。
于是对于此类看样子十分“繁杂”的问题我们可以考虑使用动态规划思想来解决问题。这题与力扣最长连续子序列的个数类似。
- 我们定义dp[i]为以i下标结尾的最长递增子序列的长度,因此我们发现其中一个动态转移方程:
dp[i] = max(dp[i],dp[j]+1)
- 并且也可以不以i下标结尾,因此有:
Max = max(dp[i],max)
- 注意max是对前面所有的dp[i-1],dp[i-2],dp[i-3]等等求一个最大值。
详解:
- 首先是对问题进行分解,要求最长递增子序列的个数,我们可以分为以最后一个元素结尾的最长递增子序列,和不以最后一个元素结尾的最长递增子序列,然后对二者求max即可。
所以拓展到任意情况同理。 - 我们可以定义dp[i]为以第i个元素结尾的最长递增子序列,由于其是以第i个结尾的,所以其会分成许多情况,即他与前面第几个元素是相连的。(因为此题可以任意相连接)由于前面有i-1个,所以枚举一下每一次,并且将其与上一次的结果比较即可。
所以可以得到状态转移方程:
dp[i] = max(dp[i],dp[j]+1); (j<=i-1)
- 由于我还可以不以第i个结尾,而是以i-1,i-2,i-3.…结尾,因此在算完每次的dp[i]后,我们又需要和前面的每一个进行比较,即和dp[i-1],dp[i-1],dp[i-3]…
- 而上述操作太麻烦,因此我们使用一个变量记录下前几个的最大值,只比较最大值和dp[i]即可。
Java代码:
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
int res = 0;
for(int i=0;i<nums.length;i++){
for(int j=0;j<i;j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[j] + 1,dp[i]);
}
}
res = Math.max(res,dp[i]);
}
return res;
}
}
C代码:
int Max(int x,int y)
{
return x>y?x:y;
}
int lengthOfLIS(int* nums, int numsSize){
int dp[numsSize+1];
if(numsSize<2)
{
return numsSize;
}
for(int i=0;i<numsSize;i++)
{
dp[i] = 1;
}
int max = dp[0];
for(int i=1;i<numsSize;i++)
{
for(int j=0;j<i;j++)
{
if(nums[j]<nums[i])
dp[i] = Max(dp[i],dp[j]+1);//由于一直进行比较,所以dp[i]本身也在不断更新
//因为不能证明dp[9999]就一定比dp[2]要大,所以需要将每种可能都筛选一遍
}
max = Max(max,dp[i]);
}
return max;
}
题解②
:
由于我们这里只要返回最长递增子序列的长度,因此我们可以采用另一种方式求解。
- 这里我们不再真正的去维护一个最长递增子序列,而是仅仅只去维护一个
递增数组
来进而维护最长递增子序列的长度。
我们维护一个递增数组res
,对于每次遍历到的nums[i]
,判断其放在数组尾部是否满足继续递增。
- 若满足直接放置即可;
- 若不满足,我们此时执行的操作是找到nums[i]应该恰好放到res数组的哪一个位置,接着使用nums[i]替代该数 (具体寻找规则如例:1 2 4 6 ,nums[i]为3,则变成 1 2 3 6) 。
之所以这样,是因为nums[i]
比起此位置的原数据来说,更小一点,他的替代或许会让后续递增序列更长,即使没有更长,因为我们执行的是覆盖操作,并没有改变原先的长度,所以此覆盖操作无后效性
,即没有副作用。
我们可以举个例子
来加深理解:
[ 1 2 3 7 9 4 5 6 8 ]
我们发现当到4时,此时应该变为了 1 2 3 4 9 ,假设他会让后续递增序列更长,那么由于后面仍旧会执行覆盖操作,因此会将不对劲的9 替换为 5
(因为9按理来说在4的前面,大小满足递增但位置不满足递增),这样看来就合理多了。
其实这里也是利用了
贪心
的思维,即我们要形成尽可能长的递增序列,
就要满足序列中的最后一个元素尽可能的小
。但是并不是一定最后一个元素为最小的就为最长递增序列了,因为还有着位置关系
这个变量作为干扰
,因此我们要在保存原先序列长度
的前提下,同时去尝试一下贪心
的方法。
代码:
class Solution {
public int lengthOfLIS(int[] nums) {
int[] res = new int[nums.length+1];
res[0] = -10000;
int len = 1;
for(int num : nums){
if(num > res[len-1]){
res[len++] = num;
continue;
}
int l = 0;
int r = len-1;
int mid = 0;
while(l<r){
mid = l + (r-l)/2;
if(num > res[mid]){
l = mid + 1;
}else{
r = mid;
}
}
res[l] = num;
}
return len-1;
}
}