【每日算法】LeetCode 152. 乘积最大子数组(动态规划)

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 152. 乘积最大子数组

1. 题目描述

1.1 问题定义

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

1.2 关键约束

  • 1 <= nums.length <= 2 * 10^4
  • -10 <= nums[i] <= 10
  • 测试用例的答案是一个32-位整数
  • 数组元素可能包含负数、零、正数

1.3 示例说明

javascript 复制代码
// 示例 1
输入: [2, 3, -2, 4]
输出: 6
解释: 子数组 [2, 3] 有最大乘积 6

// 示例 2
输入: [-2, 0, -1]
输出: 0
解释: 结果不能为 2, 因为 [-2, -1] 不是子数组

2. 问题分析

2.1 核心难点

与"最大子数组和"(LeetCode 53)不同,乘积的最大子数组问题存在独特的挑战:

  1. 负数效应:负数乘以负数得到正数,意味着当前的最小负数乘积可能在未来遇到另一个负数时变为最大正数
  2. 零值重置:遇到零时,乘积会重置为零,之前的乘积积累完全失效
  3. 符号变化:乘积的符号会随着负数的个数而变化

2.2 前端开发场景类比

前端开发中也会遇到类似场景:

  • 计算连续时间段的用户增长乘积
  • 处理股票价格连续涨跌的累计收益
  • 计算动画效果连续变化的累积影响

3. 解题思路

3.1 思路一:暴力枚举(不可行)

javascript 复制代码
// 时间复杂度:O(n²) - 对于 n=20000 会超时
// 空间复杂度:O(1)
// 评估:理论可行,实际超时

3.2 思路二:动态规划(最优解)

javascript 复制代码
// 时间复杂度:O(n)
// 空间复杂度:O(1)
// 评估:最优解,一次遍历解决问题

核心思想 :同时维护到当前位置的最大乘积最小乘积,因为最小乘积(负数)可能在遇到另一个负数时变为最大乘积。

4. 代码实现

4.1 暴力解法(理解问题)

javascript 复制代码
/**
 * 暴力解法 - 仅用于理解问题,实际会超时
 * @param {number[]} nums
 * @return {number}
 */
var maxProductBruteForce = function(nums) {
    if (nums.length === 0) return 0;
    
    let maxProduct = -Infinity;
    
    // 枚举所有可能的子数组起点
    for (let i = 0; i < nums.length; i++) {
        let currentProduct = 1;
        
        // 枚举所有可能的子数组终点
        for (let j = i; j < nums.length; j++) {
            currentProduct *= nums[j];
            maxProduct = Math.max(maxProduct, currentProduct);
        }
    }
    
    return maxProduct;
};

// 测试用例
console.log(maxProductBruteForce([2, 3, -2, 4])); // 6
console.log(maxProductBruteForce([-2, 0, -1]));   // 0

4.2 动态规划解法(标准实现)

javascript 复制代码
/**
 * 动态规划解法 - 最优解
 * @param {number[]} nums
 * @return {number}
 */
var maxProduct = function(nums) {
    if (nums.length === 0) return 0;
    
    // 初始化:第一个元素的最大和最小乘积都是它本身
    let maxSoFar = nums[0];  // 到当前位置的最大乘积
    let minSoFar = nums[0];  // 到当前位置的最小乘积
    let result = nums[0];    // 全局最大乘积
    
    // 从第二个元素开始遍历
    for (let i = 1; i < nums.length; i++) {
        const current = nums[i];
        
        // 关键步骤:同时计算三个可能性
        // 1. 当前元素本身
        // 2. 当前元素 × 之前的最大乘积
        // 3. 当前元素 × 之前的最小乘积(负数可能变正数)
        const tempMax = Math.max(
            current,                     // 情况1:重新开始
            current * maxSoFar,         // 情况2:延续最大
            current * minSoFar          // 情况3:最小变最大
        );
        
        const tempMin = Math.min(
            current,                     // 情况1:重新开始
            current * maxSoFar,         // 情况2:延续最小
            current * minSoFar          // 情况3:最大变最小
        );
        
        // 更新状态
        maxSoFar = tempMax;
        minSoFar = tempMin;
        
        // 更新全局结果
        result = Math.max(result, maxSoFar);
    }
    
    return result;
};

4.3 动态规划解法(优化版)

javascript 复制代码
/**
 * 动态规划解法 - 优化版(使用数组缓存)
 * @param {number[]} nums
 * @return {number}
 */
var maxProductOptimized = function(nums) {
    const n = nums.length;
    if (n === 0) return 0;
    
    // dpMax[i]: 以 nums[i] 结尾的子数组的最大乘积
    // dpMin[i]: 以 nums[i] 结尾的子数组的最小乘积
    const dpMax = new Array(n);
    const dpMin = new Array(n);
    
    // 初始化
    dpMax[0] = nums[0];
    dpMin[0] = nums[0];
    let result = nums[0];
    
    for (let i = 1; i < n; i++) {
        dpMax[i] = Math.max(
            nums[i],
            nums[i] * dpMax[i - 1],
            nums[i] * dpMin[i - 1]
        );
        
        dpMin[i] = Math.min(
            nums[i],
            nums[i] * dpMax[i - 1],
            nums[i] * dpMin[i - 1]
        );
        
        result = Math.max(result, dpMax[i]);
    }
    
    return result;
};

4.4 动态规划解法(空间优化版)

javascript 复制代码
/**
 * 动态规划解法 - 空间优化版(O(1)空间复杂度)
 * @param {number[]} nums
 * @return {number}
 */
var maxProductSpaceOptimized = function(nums) {
    if (nums.length === 0) return 0;
    
    let maxProduct = nums[0];
    let minProduct = nums[0];
    let result = nums[0];
    
    for (let i = 1; i < nums.length; i++) {
        // 如果当前数是负数,交换最大和最小
        // 因为负数会让大的变小,小的变大
        if (nums[i] < 0) {
            [maxProduct, minProduct] = [minProduct, maxProduct];
        }
        
        // 更新最大和最小乘积
        maxProduct = Math.max(nums[i], maxProduct * nums[i]);
        minProduct = Math.min(nums[i], minProduct * nums[i]);
        
        // 更新全局结果
        result = Math.max(result, maxProduct);
    }
    
    return result;
};

5. 各实现思路的复杂度、优缺点对比

5.1 复杂度对比表格

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
暴力枚举 O(n²) O(1) 思路简单,易于实现 效率极低,n=20000时超时 仅用于教学理解
标准动态规划 O(n) O(n) 逻辑清晰,易于理解 空间使用可以优化 一般场景
空间优化动态规划 O(n) O(1) 最优空间效率 状态转移需要仔细处理 大规模数据
交换法动态规划 O(n) O(1) 代码简洁,利用负数特性 需要理解交换逻辑 推荐使用

5.2 步骤分解说明

以输入 [2, 3, -2, 4] 为例:

步骤 i nums[i] maxSoFar minSoFar result
初始化 - - 2 2 2
第1步 1 3 max(3, 2×3, 2×3)=6 min(3, 2×3, 2×3)=3 max(2,6)=6
第2步 2 -2 max(-2, 6×-2, 3×-2)=-2 min(-2, 6×-2, 3×-2)=-12 max(6,-2)=6
第3步 3 4 max(4, -2×4, -12×4)=4 min(4, -2×4, -12×4)=-48 max(6,4)=6

最终结果:6

6. 总结

6.1 通用解题模板

javascript 复制代码
/**
 * 处理"乘积最值"类问题的通用模板
 * @param {number[]} nums
 * @return {number}
 */
function productMaxMinTemplate(nums) {
    if (nums.length === 0) return 0;
    
    // 1. 初始化状态变量
    let maxProd = nums[0];  // 当前最大乘积
    let minProd = nums[0];  // 当前最小乘积
    let result = nums[0];   // 全局结果
    
    // 2. 遍历数组
    for (let i = 1; i < nums.length; i++) {
        const current = nums[i];
        
        // 3. 根据当前值特性调整状态(如有负数需交换)
        if (current < 0) {
            [maxProd, minProd] = [minProd, maxProd];
        }
        
        // 4. 更新状态:考虑重新开始或延续
        maxProd = Math.max(current, maxProd * current);
        minProd = Math.min(current, minProd * current);
        
        // 5. 更新全局结果
        result = Math.max(result, maxProd);
    }
    
    return result;
}

6.2 核心思想总结

  1. 双重状态维护:同时跟踪最大和最小乘积
  2. 负数处理:负数会反转大小关系,需要特殊处理
  3. 零值处理:遇到零时乘积重置
  4. 重新开始可能:当前元素本身可能比延续乘积更好

6.3 类似题目推荐

题目编号 题目名称 相似点 差异点 前端应用场景
53 最大子数组和 连续子数组问题 只有加法,无需考虑符号变化 连续时间段收益计算
628 三个数的最大乘积 乘积最值问题 非连续,固定数量 商品组合推荐
713 乘积小于K的子数组 乘积相关问题 需要统计个数,不是最大 连续事件阈值统计
238 除自身以外数组的乘积 乘积计算 需要排除自身元素 数据预处理计算
相关推荐
qq_41601872几秒前
实时数据可视化库
开发语言·c++·算法
格林威几秒前
工业相机参数解析:曝光时间与运动模糊的“生死博弈”
c++·人工智能·数码相机·opencv·算法·计算机视觉·工业相机
用户908324602733 分钟前
Spring AI + RAG + SSE 实现带搜索来源的智能问答完整方案
前端·后端
GISer_Jing7 分钟前
阿里开源纯前端浏览器自动化 PageAgent,[特殊字符] 浏览器自动化变天啦?
前端·人工智能·自动化·aigc·交互
2401_8732046514 分钟前
C++中的策略模式进阶
开发语言·c++·算法
xushichao198919 分钟前
C++中的职责链模式实战
开发语言·c++·算法
清风徐来QCQ26 分钟前
js中的模板字符串
开发语言·前端·javascript
大鹏说大话26 分钟前
数据库查询优化全攻略:从索引设计到架构演进
算法
小O的算法实验室26 分钟前
2025年IEEE TETCI SCI2区,一种用于二次无约束二进制优化的协同神经动力学算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
成都渲染101云渲染666631 分钟前
Houdini+Blender高效渲染方案(高配算力+全渲染器兼容)
前端·系统架构