Leetcode.300. 最长递增子序列---DP问题/贪心+二分

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],判断其放在数组尾部是否满足继续递增。

  1. 若满足直接放置即可;
  2. 若不满足,我们此时执行的操作是找到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;
    }
}

猜你喜欢

转载自blog.csdn.net/xiangguang_fight/article/details/121453386