文章目录

  • 300. 最长递增子序列
  • 解题
  • 方法一:动态规划
  • 方法二:贪心+二分查找


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(n log(n)) 吗?

解题

方法一:动态规划

动态规划。
dp[i]=前i个元素,以第i个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取。
我们从小到大计算,计算dp[i]时 前dp[0,i-1]已经计算得到,则状态转移方程:

  • dp[i] = max(dp[j]) + 1, 0 < j <= i, num[i] > num[j]

我们的遍历思路是:两层循环,外层循环控制最长子序列到哪结束。然后内层循环从第一个元素开始遍历到外层循环控制的位置,找到此时最长的序列长度。

最后,整个数组的最长上升子序列即所有 dp[i] 中的最大值。

// , 时间O(n^2),空间O(n)
class Solution {
    public int lengthOfLIS(int[] nums) {
        int length = nums.length;
        if (length == 1) {
            return 1;
        }
        // dp[i]=前i个元素,以第i个数字结尾的最长上升子序列的长度
        int[] dp = new int[length];
        // base case 只有一个元素,长度也是1
        dp[0] = 1;
        int maxLength = 1;
        // 开始遍历
        for (int i = 1; i < length; i++) {
            // 一开始以第i个数字结尾的最长上升子序列的长度,最小就是1
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j]+1);
                }
            }
            maxLength = Math.max(maxLength, dp[i]);
        }

        return maxLength;

    }
}

方法二:贪心+二分查找

考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个数组 d[i] (数组是严格递增的),表示长度为 i 的最长上升子序列的末尾元素的最小值,用 len 记录目前最长上升子序列的长度,起始时 len 为 1,d[1]=nums[0]。

为了维护这个辅助数组的性质,在扫描主数组时:

  • 如果遇到一个比 d 的尾部元素更大的值,说明形成了一个更长的上升序列。 则把它追加到 d 的尾部。
  • 如果遇到一个比 d 的尾部元素更小的值,记为a,说明发现了某个上升子序列的、更小的末尾元素,需要更新它。所以我们需要在目前的d这个严格递增的数组中,找到第一个比a大的元素,记为b,然后用a替换b。这个搜索过程可以用二分查找。

最后,d数组的长度就是答案。

此方法为什么可行?分析:
“遍历给定的数组 nums,对于每个元素 i,找到d中第一个大于等于 i 的元素 it。如果it存在,要用i替换it。“ 原因有两点:

  • 一是,此时用i替换it,并不影响当前最长增长子序列的实际长度。其实无论怎么替换当前d[]中的存在元素都不影响当前最长子序列的实际长度(只有另一种情况时,往末尾增加新元素才会改变)。
  • 二是,之所以用i替换it,是为了得到一种“增益”,使得当前最长增长子序列序列增长的更慢一点。这是关键所在。
//时间O(n log(n))  空间O(n)
class Solution {
    public int lengthOfLIS(int[] nums) {
        int length = nums.length;
        if (length == 1) {
            return 1;
        }
        // 数组 d[i] (数组是严格递增的),表示长度为 i 的最长上升子序列的末尾元素的最小值
        // 所以数组的长度应是length+1; 才能满足记录长度从1到length的最长上升子序列
        int[] d = new int[length+1];
        int len = 1;
        d[len] = nums[0];
        // 开始遍历原数组
        for (int i = 1; i < length; i++) {
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
            } else {
                // 当前元素小于d数组末尾元素,在d数组中查找位置
                int l = 1, r = len;
                // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                // pos代表第一个比nums[i] 小的下标,找到之后pos+1就是nums[i]的位置。
                int pos = 0;
                while (l <= r) {
                    int mid = (l+r)/2;
                    if (nums[i] > d[mid]) {
                        pos = mid;
                        l = mid+1;
                    } else {
                        r = mid-1;
                    }
                }
                d[pos+1] = nums[i];
            }
        }
        return len;
    }
}