leetcode----300.最长递增子序列

300.最长递增子序列

问题:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

思路:

  • 动态规划

第一步,定义dp数组的含义。定义dp[i]表示以第i个元素结尾的最长递增子序列的长度。

第二步,确定状态转移方程。假设需要求以第i个元素结尾的最长递增子序列的长度,即dp[i]。此时我们需要找到前i个元素中,对应元素值小于第i个元素值,且以这个元素结尾的最长递增子序列长度最大,再在这个子序列末尾加上第i个元素nums[i]。我们可以写出这样的状态转移方程。
d p [ i ] = m a x ( d p [ j ] ) + 1 , 0 ≤ j ≤ i a n d n u m s [ i ] > n u m s [ j ] dp[i]=max(dp[j])+1, \quad 0\leq j \leq i \quad and \quad nums[i] > nums[j] dp[i]=max(dp[j])+1,0jiandnums[i]>nums[j]

第三步,初始化dp数组,dp[0]=1,这个很容易知道,另外,由于我只会在nums[i] > nums[j]时更新dp[i]的值,所以再寻找最大的dp[j]时,会先令dp[i]=1,若前i个元素中不存在递增的子序列,dp[i]=1,若有则会更新dp[i]的值。防止出现dp[i]=0的情况。

最终所求的结果为 m a x ( d p [ i ] ) 0 ≤ i ≤ n u m s . l e n g t h max(dp[i]) \quad 0\leq i \leq nums.length max(dp[i])0inums.length.

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = nums.length;
        if(nums.length == 0 || nums.length == 1) return nums.length;

        int[] dp = new int[len];
        int max = 0;
        dp[0] = 1;
        for(int i = 1; i < len; i++){
            dp[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}
  • 贪心 + 二分查找

贪心策略:若我们想要找到的递增子序列尽可能长,那么我们需要让每次递增的幅度尽可能小。

因此使用dp[i]表示长度为i的最长递增子序列末尾元素最小值。使用len记录目前最长递增子序列的长度。

接下来证明dp数组的单调性,反证法:假设有dp[j] > dp[i]j < i。将长度为i的最长递增子序列的后i-j个元素删去,我们可以得到一个长度为j的最长递增子序列,则必有该子序列的第j个元素小于dp[i],根据假设,也小于dp[j]。这样我们就找到了一个长度为j的最长递增子序列,且其末尾元素比dp[j]小,这与我们定义的dp数组含义相矛盾,所以dp数组应该是单调递增的。最后得到的dp数组其实就是所求的最长递增子序列。

算法流程:我们需要从前往后的遍历数组nums,假设当前元素为nums[i],当前的最长递增子序列长度为len。则有两种情况

  • nums[i] > dp[len],则直接将nums[i]添加到dp数组的末尾,同时len的长度加1.
  • nums[i] <= dp[len]从后往前遍历当前dp数组,找到第一个小于nums[i]的元素dp[j],并更新dp[j+1] = nums[i]。由于dp数组是单调的,所以可以使用二分查找来优化时间复杂度。
class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length == 0 || nums.length == 1) return nums.length;

        int len = 1;
        int[] dp = new int[nums.length + 1];
        dp[1] = nums[0];
        for(int i = 1; i < nums.length; i++){
            if(nums[i] > dp[len]){
                dp[++len] = nums[i];
            } else {
                int l = 1, r = len, pos = 0;//若所有元素都大于nums[i],则更新dp数组的第一个元素
                while(l <= r){
                    int mid = l + (int) (r - l) / 2;
                    if(dp[mid] < nums[i]){
                        pos = mid;
                        l = mid + 1;
                    } else{
                        r = mid - 1;
                    }
                }
                dp[pos + 1] = nums[i];
            }
        }
        return len;
    }
}

另外,我觉得二分查找需要注意的两个细节

  • while循环结束的条件,我们假设搜索区间是一个长度为len的数组
    • l <= r,表示整个搜索区间是一个闭区间[l , r],此时初始的右边界应该为len - 1,是数组最后一个元素的索引
    • l < r,表示整个搜索区间是一个左闭右开区间[l , r),此时初始的右边界应该是len, 即索引为len是越界的。
  • 每次搜索区间的边界变化
    • 因为这道题目里面,每次搜索前,已经判断过mid位置对应的元素了,下一次搜索时,应该将mid从搜索区间去掉。所以这里是l = mid + 1r = mid - 1

参考官方题解总结的思路,若哪里理解有误,望指正。欢迎讨论。

你可能感兴趣的:(#,leetcode,leetcode,动态规划,算法,数据结构)