LeetCode 300. 最长递增子序列 动态规划 + 贪心 详解

300. 最长递增子序列

题目来源

300. 最长递增子序列

题目分析

给定一个整数数组 nums,要求找到其中最长严格递增子序列的长度。严格递增子序列指的是对于子序列中的每一对相邻元素 nums[i]nums[j],均满足 i < jnums[i] < nums[j]

题目难度

  • 难度:中等

题目标签

  • 标签:动态规划

题目限制

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4

进阶

你能否设计出时间复杂度为 O(n^2) 或更优的解决方案?

解题思路

思路1:动态规划

  1. 问题定义:设 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。

  2. 状态转移

    • 对于每个 i,遍历从 0i-1 的所有元素 nums[j],如果 nums[i] > nums[j],则 dp[i] 可以更新为 dp[j] + 1
    • dp[i] = max(dp[j]) + 1,其中 j < inums[j] < nums[i]
  3. 初始化

    • dp 数组初始值为 1,因为每个元素单独作为一个子序列时,长度为 1
  4. 最终结果

    • 最后返回 dp 数组中的最大值,即为最长递增子序列的长度。

思路2:贪心 + 二分查找

  1. 核心思想

    • 通过维护一个数组 g,用以保存当前能够形成的最长递增子序列的最小结尾元素,g[i]表示长度为i+1的子序列的最小末尾元素。
  2. 算法步骤

    • 遍历 nums 数组中的每个元素 nums[i],在数组 g 中找到第一个大于等于 nums[i] 的元素并替换之。如果 nums[i]g 中所有元素都大,则将其追加到 g 的末尾。
  3. 优化

    • 二分查找可以有效地找到 g 中需要替换的位置,从而将时间复杂度优化为 O(n log n)

核心算法步骤

  1. 动态规划法

    • 初始化 dp 数组为全 1
    • 通过两重循环更新 dp 数组。
    • 返回 dp 数组中的最大值。
  2. 贪心 + 二分查找法

    • 使用二分查找找到需要更新的 g 数组位置。
    • 更新或追加 g 数组。
    • 返回 g 数组的长度。

代码实现

以下是求解最长递增子序列问题的 Java 代码:

方法1:动态规划
/**
 * 300. 最长递增子序列
 * @param nums 整数数组
 * @return 最长递增子序列的长度
 */
public int lengthOfLIS(int[] nums) {
    int ans = 0;
    int n = nums.length;
    int[] dp = new int[n];
    for (int i = 0; i < n; i++) {
        dp[i] = 1;
    }
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    for (int i = 0; i < n; i++) {
        ans = Math.max(ans, dp[i]);
    }
    return ans;
}
方法2:贪心 + 二分查找
/**
 * 300. 最长递增子序列
 * @param nums 整数数组
 * @return 最长递增子序列的长度
 */
public int lengthOfLIS2(int[] nums) {
    int n = nums.length;
    int[] g = new int[n];
    g[0] = nums[0];
    int len = 1;
    for (int i = 1; i < n; i++) {
        int l = 0, r = len - 1;
        while (l <= r) { // [l,r)
            int mid = l + r >>> 1;
            if (g[mid] < nums[i]) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        g[l] = nums[i];
        if (l == len) {
            len++;
        }
    }
    return len;
}

代码解读

  • lengthOfLIS 方法

    • 通过双层循环,遍历所有 nums[i] 之前的元素 nums[j],找到最长递增子序列并更新 dp
  • lengthOfLIS2 方法

    • 通过二分查找来确定 g 数组中第一个大于等于 nums[i] 的位置,并进行更新或追加。

性能分析

  • 动态规划法

    • 时间复杂度:O(n^2),其中 n 是数组 nums 的长度。
    • 空间复杂度:O(n),因为我们使用了 dp 数组。
  • 贪心 + 二分查找法

    • 时间复杂度:O(n log n),因为二分查找的引入将时间复杂度优化为 O(log n)
    • 空间复杂度:O(n),因为我们使用了 g 数组。

测试用例

你可以使用以下测试用例来验证代码的正确性:

int[] nums1 = {10,9,2,5,3,7,101,18};
int result1 = lengthOfLIS(nums1);
System.out.println(result1); // 输出: 4

int[] nums2 = {0,1,0,3,2,3};
int result2 = lengthOfLIS(nums2);
System.out.println(result2); // 输出: 4

int[] nums3 = {7,7,7,7,7,7,7};
int result3 = lengthOfLIS(nums3);
System.out.println(result3); // 输出: 1

扩展讨论

其他实现

  • 递归 + 记忆化
    • 可以通过递归加记忆化的方法来解决该问题,但可能在时间复杂度上没有二分查找法优化得好。

总结

最长递增子序列问题通过动态规划和贪心 + 二分查找两种方法来解决。动态规划法简单直观,但时间复杂度较高,而贪心 + 二分查找法在时间复杂度上具有优势,适用于数据规模较大的情况。


  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值