二刷 LeetCode:300. 最长递增子序列 & 152. 乘积最大子数组 复盘笔记

目录

[一、300. 最长递增子序列](#一、300. 最长递增子序列)

题目回顾

思路复盘

[方法 1:基础动态规划(O (n²))](#方法 1:基础动态规划(O (n²)))

[方法 2:二分查找优化(O (n log n))](#方法 2:二分查找优化(O (n log n)))

[易错点 & 二刷心得](#易错点 & 二刷心得)

[二、152. 乘积最大子数组](#二、152. 乘积最大子数组)

题目回顾

思路复盘

核心思路:同时维护最大值和最小值

[易错点 & 二刷心得](#易错点 & 二刷心得)

[三、两道题的共性总结 & 二刷收获](#三、两道题的共性总结 & 二刷收获)


这两道题都是中等难度的经典动态规划题,第一次刷的时候很容易踩坑,二刷时我们重点拆解思路、优化写法,顺便把易错点和通用模板总结清楚。


一、300. 最长递增子序列

题目回顾

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。

思路复盘

方法 1:基础动态规划(O (n²))

这是最直观的解法,核心是定义状态和状态转移方程。

  1. 状态定义dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。
  2. 状态转移
    • 对于每个 i,遍历 j ∈ [0, i-1]
    • 如果 nums[i] > nums[j],说明可以把 nums[i] 接在 nums[j] 后面,形成更长的递增子序列
    • 状态转移方程:dp[i] = max(dp[i], dp[j] + 1)
  3. 初始状态 :每个元素本身都是长度为 1 的子序列,所以 dp[i] = 1
  4. 结果dp 数组中的最大值

Java 代码实现

java

运行

复制代码
public int lengthOfLIS(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    int n = nums.length;
    int[] dp = new int[n];
    Arrays.fill(dp, 1);
    int maxLen = 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);
            }
        }
        maxLen = Math.max(maxLen, dp[i]);
    }
    return maxLen;
}
方法 2:二分查找优化(O (n log n))

进阶优化思路,用一个数组维护最小结尾元素

  • tails[i] 表示长度为 i+1 的递增子序列的最小可能结尾元素
  • 遍历 nums,对每个元素 num
    1. 如果 num 大于 tails 最后一个元素,直接追加到末尾,子序列长度 + 1
    2. 否则,用二分查找找到 tails 中第一个大于等于 num 的位置,替换它(保证后续有更长的递增可能)
  • 最终 tails 的长度就是最长递增子序列的长度

Java 代码实现

java

运行

复制代码
public int lengthOfLIS(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    int[] tails = new int[nums.length];
    int len = 0;
    for (int num : nums) {
        int left = 0, right = len;
        while (left < right) {
            int mid = (left + right) / 2;
            if (tails[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        tails[left] = num;
        if (left == len) {
            len++;
        }
    }
    return len;
}

易错点 & 二刷心得

  1. DP 方法的边界dp 数组的初始值必须全为 1,因为每个元素本身就是一个子序列。
  2. 二分方法的误区tails 数组并不是最终的最长递增子序列,它只是维护了最小结尾,长度才是答案。
  3. 适用场景:O (n²) 适合数据量较小的场景,O (n log n) 是面试中的进阶优化点,建议两种方法都掌握。

二、152. 乘积最大子数组

题目回顾

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

思路复盘

这道题和「最大子数组和」很像,但因为负数和 0 的存在,直接用单变量 DP 会翻车:负数乘以负数会变成正数,之前的最小值可能变成最大值。

核心思路:同时维护最大值和最小值
  1. 状态定义

    • maxDp[i]:以 nums[i] 结尾的子数组的最大乘积
    • minDp[i]:以 nums[i] 结尾的子数组的最小乘积(因为负负得正,最小值可能变成最大值)
  2. 状态转移

    • 对于每个 nums[i],有三种选择:

      1. 只取当前元素 nums[i]
      2. 用之前的最大值乘当前元素:maxDp[i-1] * nums[i]
      3. 用之前的最小值乘当前元素:minDp[i-1] * nums[i]
    • 状态转移方程: plaintext

      复制代码
      maxDp[i] = max(nums[i], maxDp[i-1] * nums[i], minDp[i-1] * nums[i])
      minDp[i] = min(nums[i], maxDp[i-1] * nums[i], minDp[i-1] * nums[i])
  3. 初始状态maxDp[0] = minDp[0] = nums[0]

  4. 结果:遍历过程中记录的最大值

Java 代码实现(优化空间版)

java

运行

复制代码
public int maxProduct(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    int max = nums[0], min = nums[0], result = nums[0];
    for (int i = 1; i < nums.length; i++) {
        // 保存之前的max,避免被覆盖
        int prevMax = max;
        int prevMin = min;
        max = Math.max(nums[i], Math.max(prevMax * nums[i], prevMin * nums[i]));
        min = Math.min(nums[i], Math.min(prevMax * nums[i], prevMin * nums[i]));
        result = Math.max(result, max);
    }
    return result;
}

易错点 & 二刷心得

  1. 为什么要同时维护最小值? 比如 nums = [-2, 3, -4],第一次计算到 3 时,最小值是 - 6,第二次乘以 - 4,得到 24,这就是最大值。
  2. 空间优化技巧 :不需要完整的 dp 数组,只需要用两个变量保存上一次的最大值和最小值即可,空间复杂度从 O (n) 降到 O (1)。
  3. 0 的处理:当遇到 0 时,max 和 min 都会被重置为 0,后续计算会重新开始,不影响结果。

三、两道题的共性总结 & 二刷收获

  1. 动态规划的核心:定义清晰的状态、找到正确的状态转移方程,这是解决 DP 题的关键。
  2. 特殊情况的处理
    • 最长递增子序列:二分优化的核心是维护最小结尾,这是贪心 + 二分的经典结合。
    • 乘积最大子数组:必须同时维护最大和最小值,应对负数带来的反转问题。
  3. 优化意识:二刷时要从暴力解法出发,思考如何优化时间 / 空间复杂度,比如把 O (n²) 降到 O (n log n),把 O (n) 空间降到 O (1)。
相关推荐
我是大聪明.1 小时前
大模型Tokenizer原理:BPE、WordPiece与子词编码的核心机制深度解析
人工智能·线性代数·算法·机器学习·矩阵
自我意识的多元宇宙1 小时前
数据结构----希尔排序
数据结构·算法·排序算法
6Hzlia1 小时前
Hot 100 刷题计划】 LeetCode 146. LRU 缓存 | C++ 哈希表+双向链表
c++·leetcode·缓存
惊鸿一博1 小时前
深度学习特征匹配算法 LoFTR、DKM、RoMa 介绍
人工智能·深度学习·算法
炽烈小老头1 小时前
【每天学习一点算法 2026/04/29】最长连续序列
学习·算法
一只数据集1 小时前
柏林道路路面图像数据集-971张沥青与鹅卵石路面图片-训练测试集划分-支持道路材质识别与自动驾驶视觉算法训练
算法·自动驾驶·材质
我不是懒洋洋1 小时前
【数据结构】二叉树OJ(单值二叉树、检查两棵树是否相同、对称二叉树、二叉树的前序遍历、另一颗树的子树)
c语言·数据结构·c++·经验分享·算法·leetcode·visual studio
wljy12 小时前
每日一题(2026.4.29) 猫猫与数学
c语言·c++·算法·蓝桥杯·stl·牛客
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章56-OCR
图像处理·人工智能·opencv·算法·计算机视觉·ocr