Skip to content

最长递增子序列

约 1868 字大约 6 分钟

动态规划贪心二分

2025-02-27

总之,思想就是让 cell 中存储比较小的元素。这样,cell 未必是真实的最长上升子序列,但长度是对的。

子串和子序列的区别在于:子串是连续的,所以只需要较当前元素和前一个元素,如果前一个元素比当前元素大,那就没有当前元素的事了,直接研究子问题就可以了。子序列不是这样,比较完当前元素和前一个元素,如果前一个元素较大,不能直接丢弃当前元素,还可以和前面的其他元素比较,因为子序列不要求连续。

想理解二分查找的做法,可以看这个例子: 1 4 5 3,我们需要构造一个递增双边队列。一开始把 1 加进去,4比1大,所以也直接加到队尾,5比4大,所以直接加到队尾,3比5小,所以需要查找3可以插入的位置,查找完是1的位置,所以用3把1替换掉,之所以要查找替换,是因为我们不在乎序列是什么,我们只在乎序列的长度最长是多少。而要想序列最长,需要各个数字之间的距离最短,而替换的数一定大于被替换的数,所以我们选择替换掉小的数。不能直接插入,不然会改变数字出现的顺序!

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)) 吗?

动态规划

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        // dp数组用于存储到当前位置为止的最长递增子序列的长度
        int[] dp = new int[n];

        // // 错误的 base case
        // dp[0] = 1;

        // // 错误的状态转移
        // for (int i = 1; i < n; i++) {
        //     // 不要只关注前一个,要关注前面所有的数,因为我们要的不是连续的字串。如果要连续的话,那就只关注前一个。
        //     dp[i] = nums[i] > nums[i - 1] ? dp[i - 1] + 1 : dp [i - 1];
        // }

        // 初始化dp数组,每个位置的最长递增子序列长度至少为1(包含自身)。重点
        Arrays.fill(dp, 1);

        // 状态转移
        for (int i = 0; i < nums.length; i++) {
            // 遍历i位置之前的所有元素,寻找可以与nums[i]构成递增子序列的元素。重点。
            for (int j = 0; j < i; j++) {
                // 如果nums[i]大于nums[j],则nums[i]可以接在nums[j]之后构成更长的递增子序列
                // 有可能这一轮没有满足条件的 j,所以 dp[i] 就无法更新。万一下一轮需要用 dp[i] 来更新 dp[i + 1],
                // 那就做不到。因此我们需要事先把 dp[] 全都初始化为 1.
                if (nums[i] > nums[j])
                    // 更新dp[i],取当前dp[i]与dp[j]+1的较大值
                    // dp[j] + 1表示在nums[j]的最长递增子序列后加上nums[i]构成新的递增子序列
                    dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }

        // 遍历dp数组找到最长递增子序列的长度
        int res = 0;
        for (int j : dp) {
            // res记录dp数组中的最大值,即整个数组的最长递增子序列的长度
            res = Math.max(res, j);
        }
        
        // 返回最长递增子序列的长度
        return res;
    }
}
  • 时间复杂度:O(n^2),双层遍历
  • 空间复杂度:O(n)

贪心+二分+动态规划

想理解二分查找的做法,可以看这个例子:1 4 7 5 6,我们需要构造一个递增双边队列。一开始把 1 加进去,4比1大,所以也直接加到队尾, 7比4大,所以直接加到队尾,5比7小,所以需要查找5可以插入的位置,查找完是7的位置,所以用5把7替换掉,之所以要查找替换, 是因为我们不在乎序列是什么,我们只在乎序列的长度最长是多少。

而要想序列最长,需要各个数字之间的距离最短,而替换的数一定大于被替换的数,所以我们选择替换掉小的数。

不能直接插入,不然会改变数字出现的顺序!

class Solution {
    public int lengthOfLIS(int[] nums) {
        int size = nums.length;
        // 如果数组的长度小于2,直接返回数组的长度即为最长递增子序列的长度
        if (size < 2) return size;
        
        // arr数组用来维护找到的最长递增子序列
        int[] arr = new int[size];
        // 初始时,将nums的第一个元素放入arr中
        arr[0] = nums[0];
        // arr数组中实际存入的值的数量,即当前最长递增子序列的长度,初始为1
        int res = 1;
        
        for (int i = 1; i < size; i++) {
            // 如果当前元素大于arr数组的最后一个元素,则直接追加到arr末尾
            // 这表示我们找到了一个更长的递增子序列
            if (nums[i] > arr[res - 1]) {
                arr[res] = nums[i];
                // 更新最长递增子序列的长度
                res++;
                continue;
            }

            // 如果当前元素不大于arr的最后一个元素,使用二分查找法找到arr中第一个不小于当前元素的位置,即当前元素插入的位置。
            // 二分结束时,right右边(不包括right)全是大于等于指定数的数,
            // left左边(不包括left)全是小于等于指定数的数,因此我们只需检查最后这个数[left,left+1]即可。寻找大于等于nums[i]的左边界。
            int left = 0, right = res - 1, mid;
            while (left <= right) {
                mid = left + (right - left) / 2;
                // 如果arr[mid]大于等于当前元素,则在左侧区间继续查找
                if (arr[mid] >= nums[i]) {
                    right = mid - 1;
                } else {
                    // 否则在右侧区间查找
                    left = mid + 1;
                }
            }
            // 将找到的位置(left指针的位置)的元素更新为当前元素
            // 这样做是为了保持arr数组的长度代表的最长递增子序列的潜在可能性。注意是left而不是left-1
            arr[left] = nums[i];
        }
        // 返回最终计算的最长递增子序列的长度
        return res;
    }
}

插入数据替换比它大的最小的那个,是为了让ceil数组的最后一个元素变得更小,这样ceil数组才能变得更长。(ceil数组最后一个元素永远是正确的,内部其他元素可能不正确,但是不影响长度)

  • 时间复杂度 O(NlogN):遍历 nums 列表需 O(N),在每个 nums[i] 二分法需 O(logN)。
  • 空间复杂度 O(N):arr 数组占用线性大小额外空间。