两道经典子序列 / 子数组 DP 题:最长递增子序列 & 乘积最大子数组

前言

动态规划里,「子序列」和「子数组」是高频考点,很多同学容易把它们搞混。今天我们就用两道中等难度的经典题,把这两个概念和对应的 DP 解法讲透:一道是 **《最长递增子序列》,一道是《乘积最大子数组》**。


一、最长递增子序列(LeetCode 300)

题目描述

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

核心思路:一维 DP + 优化

这道题是子序列 DP 的入门标杆题,核心是通过「前序状态」推导出当前状态。

状态定义

dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。

转移方程

对于每个 i,遍历所有 j < i:如果 nums[i] > nums[j],则 dp[i] = max(dp[i], dp[j] + 1)

边界条件
  • 每个元素自身都是一个长度为 1 的子序列,所以 dp[i] 初始化为 1
  • 最终答案是 dp 数组中的最大值

代码实现(Java 版)

java

运行

复制代码
public class LengthOfLIS {
    // 基础DP解法(O(n²))
    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;
    }

    // 优化解法(贪心+二分,O(n log n))
    public int lengthOfLISOptimized(int[] nums) {
        List<Integer> tails = new ArrayList<>();
        for (int num : nums) {
            // 找到第一个 >= num 的位置,替换为 num
            int idx = Collections.binarySearch(tails, num);
            if (idx < 0) idx = -idx - 1;
            if (idx == tails.size()) {
                tails.add(num);
            } else {
                tails.set(idx, num);
            }
        }
        return tails.size();
    }

    public static void main(String[] args) {
        LengthOfLIS solution = new LengthOfLIS();
        int[] nums = {10,9,2,5,3,7,101,18};
        System.out.println(solution.lengthOfLIS(nums)); // 输出:4
        System.out.println(solution.lengthOfLISOptimized(nums)); // 输出:4
    }
}

关键知识点

  • 时间复杂度:基础 DP 为 O (n²),优化后为 O (n log n)
  • 空间复杂度:基础 DP 为 O (n),优化后为 O (n)
  • 核心区别:子序列不要求连续,所以需要遍历所有前序元素,而不是只看相邻元素

二、乘积最大子数组(LeetCode 152)

题目描述

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

核心思路:维护最大 / 最小值的 DP

这道题的坑点在于负数:负负得正,所以当前的最小值(负数)乘以一个负数,反而可能变成最大值。因此,我们不能只维护当前的最大值,还要维护当前的最小值。

状态定义
  • maxDp[i]:以 nums[i] 结尾的乘积最大子数组的乘积
  • minDp[i]:以 nums[i] 结尾的乘积最小子数组的乘积
转移方程
  • 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])
边界条件

maxDp[0] = minDp[0] = nums[0],最终答案是 maxDp 数组中的最大值

代码实现(Java 版)

java

运行

复制代码
public class MaxProduct {
    // 基础DP解法
    public int maxProduct(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        int n = nums.length;
        int[] maxDp = new int[n];
        int[] minDp = new int[n];
        maxDp[0] = minDp[0] = nums[0];
        int maxRes = nums[0];
        
        for (int i = 1; i < n; i++) {
            maxDp[i] = Math.max(nums[i], Math.max(maxDp[i-1] * nums[i], minDp[i-1] * nums[i]));
            minDp[i] = Math.min(nums[i], Math.min(maxDp[i-1] * nums[i], minDp[i-1] * nums[i]));
            maxRes = Math.max(maxRes, maxDp[i]);
        }
        return maxRes;
    }

    // 优化版(空间O(1))
    public int maxProductOptimized(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        int max = nums[0], min = nums[0], res = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            int currMax = Math.max(nums[i], Math.max(max * nums[i], min * nums[i]));
            int currMin = Math.min(nums[i], Math.min(max * nums[i], min * nums[i]));
            max = currMax;
            min = currMin;
            res = Math.max(res, max);
        }
        return res;
    }

    public static void main(String[] args) {
        MaxProduct solution = new MaxProduct();
        int[] nums = {2,3,-2,4};
        System.out.println(solution.maxProduct(nums)); // 输出:6
        System.out.println(solution.maxProductOptimized(nums)); // 输出:6
    }
}

关键知识点

  • 时间复杂度:O (n),仅遍历一次数组
  • 空间复杂度:基础 DP 为 O (n),优化后为 O (1)
  • 核心区别:子数组必须连续,所以只需要看前一个状态,而不是所有前序状态
相关推荐
IT大白鼠3 小时前
AIGC性能的关键瓶颈:算力、数据、算法三者如何互相制约?
算法·aigc
白雪茫茫3 小时前
监督学习、半监督学习、无监督学习算法详解
python·学习·算法·ai
FengyunSky3 小时前
浅析 空间频率响应 SFR 计算
算法
树下水月4 小时前
PHP 一种改良版的雪花算法
算法·php·dreamweaver
一只数据集4 小时前
全尺寸人形机器人灵巧手力觉触觉数据集-2908条ROSbag数据覆盖14大应用场景深度解析
大数据·人工智能·算法·机器人
罗西的思考5 小时前
【GUI-Agent】阿里通义MAI-UI 代码阅读(2)--- 实现
人工智能·算法·机器学习
刀法如飞7 小时前
TypeScript 数组去重的 20 种实现方式,哪一种你还不知道?
前端·javascript·算法
sali-tec7 小时前
C# 基于OpenCv的视觉工作流-章66-直线夹角
图像处理·人工智能·opencv·算法·计算机视觉
AC赳赳老秦7 小时前
接口测试自动化:用 OpenClaw 对接 Postman,实现批量回归测试、测试报告自动生成与推送
java·人工智能·python·算法·elasticsearch·deepseek·openclaw
_风满楼8 小时前
TDD实战-会议室冲突检测的红绿重构循环
前端·javascript·算法