当前位置: 首页 > news >正文

LeetCode 152. 乘积最大子数组:从双状态DP到空间优化【C++/Java精讲】

1. 问题引入:为什么乘积最大子数组这么难?

第一次看到LeetCode 152题时,我心想:"这不就是最大子数组和的变种吗?"结果被负数狠狠教育了。还记得当时用最大子数组和的思路写代码,遇到[2,-3,-2,4]直接翻车——正确答案应该是48(全数组乘积),但我的代码却返回6(前两个数乘积)。

关键矛盾点在于:乘积具有符号敏感性。一个负数能让最大值瞬间变最小值,也能让最小值逆袭成最大值。举个例子:

  • 当前最大值是6,遇到-2:6 × (-2) = -12(变成最小值)
  • 当前最小值是-3,遇到-2:-3 × (-2) = 6(反而成了最大值)

这就像坐过山车,必须同时记录最高点和最低点,才能应对突然的下坠或上升。这也是为什么简单的单状态DP会失效,必须引入双状态动态规划

2. 双状态DP的核心思想

2.1 状态定义的艺术

传统DP通常用f[i]表示以nums[i]结尾的子数组最优解,但在这里我们需要两个数组:

  • maxDP[i]:以nums[i]结尾的子数组最大乘积
  • minDP[i]:以nums[i]结尾的子数组最小乘积

为什么需要最小值?看这个例子:

nums = [3, -2, -4]

当处理到-4时:

  • 前一步的最大值是3 × (-2) = -6
  • 前一步的最小值就是-2本身
  • -6 × (-4) = 24(新的最大值来自最小值×负数)

2.2 状态转移方程推导

状态转移需要考虑三种情况(以maxDP[i]为例):

  1. 自立门户:从当前数字重新开始(nums[i]
  2. 继承遗产maxDP[i-1] * nums[i](正数乘正数)
  3. 逆袭翻盘minDP[i-1] * nums[i](负数乘负数)

用数学表达式就是:

maxDP[i] = \max(nums[i],\ maxDP[i-1]×nums[i],\ minDP[i-1]×nums[i]) minDP[i] = \min(nums[i],\ minDP[i-1]×nums[i],\ maxDP[i-1]×nums[i])

2.3 初始化与边界处理

初始状态很简单:

  • 当i=0时,子数组只能是nums[0]本身:
maxDP[0] = minDP[0] = nums[0];

但要注意数组越界问题。有次我写Java代码时没检查空数组,直接nums[0]导致崩溃。完整初始化应该:

if (nums.length == 0) return 0; int res = nums[0];

3. C++与Java实现对比

3.1 C++实现细节

class Solution { public: int maxProduct(vector<int>& nums) { if (nums.empty()) return 0; int res = nums[0], n = nums.size(); vector<int> maxDP(n), minDP(n); maxDP[0] = minDP[0] = nums[0]; for (int i = 1; i < n; ++i) { maxDP[i] = max(nums[i], max(maxDP[i-1]*nums[i], minDP[i-1]*nums[i])); minDP[i] = min(nums[i], min(minDP[i-1]*nums[i], maxDP[i-1]*nums[i])); res = max(res, maxDP[i]); } return res; } };

性能特点

  • vector的连续内存访问效率高
  • 注意max的三重嵌套调用,可以拆分成两步更清晰

3.2 Java实现注意点

class Solution { public int maxProduct(int[] nums) { if (nums.length == 0) return 0; int res = nums[0]; int[] maxDP = new int[nums.length]; int[] minDP = new int[nums.length]; maxDP[0] = minDP[0] = nums[0]; for (int i = 1; i < nums.length; 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(minDP[i-1]*nums[i], maxDP[i-1]*nums[i])); res = Math.max(res, maxDP[i]); } return res; } }

易错点

  • Java数组初始化自动填0,但我们的逻辑不需要这个特性
  • Math.max只支持两个参数,需要嵌套调用

4. 空间优化:从O(n)到O(1)

4.1 为什么可以优化?

观察状态转移方程发现:maxDP[i]minDP[i]只依赖于前一个状态。就像斐波那契数列,我们不需要保存整个数组,只需维护滚动变量

4.2 优化后的C++实现

int maxProduct(vector<int>& nums) { if (nums.empty()) return 0; int res = nums[0], maxP = nums[0], minP = nums[0]; for (int i = 1; i < nums.size(); ++i) { int currMax = max(nums[i], max(maxP*nums[i], minP*nums[i])); int currMin = min(nums[i], min(minP*nums[i], maxP*nums[i])); res = max(res, currMax); maxP = currMax; // 注意要先更新res再覆盖变量! minP = currMin; } return res; }

关键技巧

  • 使用currMaxcurrMin作为临时变量
  • 更新顺序很重要:先计算→更新res→最后覆盖旧值

4.3 Java优化版

public int maxProduct(int[] nums) { if (nums.length == 0) return 0; int res = nums[0], maxP = nums[0], minP = nums[0]; for (int i = 1; i < nums.length; i++) { int preMax = maxP; // 必须保存旧值! maxP = Math.max(nums[i], Math.max(maxP*nums[i], minP*nums[i])); minP = Math.min(nums[i], Math.min(minP*nums[i], preMax*nums[i])); res = Math.max(res, maxP); } return res; }

踩坑记录

  • 直接使用maxP计算minP会导致值被覆盖,必须先用preMax保存旧值
  • 实测下来空间消耗从40MB降到38MB,虽然不多但算法更优雅

5. 测试用例设计技巧

好的测试用例能帮你发现90%的bug,我总结了几类必测场景:

用例类型示例输入预期输出检查目标
全正数[2,3,4]24基础功能
含单个负数[2,-3,4]4负数中断连续性
负号反转[-2,3,-4]24最小值变最大值
含零[2,0,3]3零值重置
全负数[-2,-3,-1]6负负得正
单元素[5]5边界条件

特别建议测试[3,-1,4,-1,2]这个案例,最优解是48(全部相乘),能检验算法是否考虑全局乘积。

http://www.jsqmd.com/news/628438/

相关文章:

  • Graphormer模型C++高性能推理接口开发教程
  • 如何用Mermaid在线编辑器3分钟创建专业图表:新手完整指南
  • Streamlit:CSS实战——从st.markdown到st.html的样式进阶
  • 3分钟掌握:零代码TikTok评论采集终极指南
  • Qwen3-0.6B-FP8快速上手:OpenAI风格API调用chat端点示例代码
  • 专业级Android设备完整性检测:Play Integrity API Checker的5大实战应用场景
  • ConvNeXt 系列改进:独家首发:ConvNeXt 引入频率域注意力(FreqAttention),提升纹理敏感任务
  • 【节点】[Multiply节点]原理解析与实际应用
  • 如何在5分钟内掌握Dell G15开源散热控制神器:tcc-g15终极指南
  • AMD Ryzen系统调试终极指南:5个实用场景掌握SMUDebugTool
  • Pijul:基于补丁理论的分布式版本控制系统新突破
  • 2026年4月不锈钢法兰源头厂家选哪家,不锈钢法兰/不锈钢美标法兰/304法兰/不锈钢锻件法兰,不锈钢法兰公司推荐分析 - 品牌推荐师
  • OpenClaw进阶实战(十二):电商比价工作流(一)——数据采集与竞品监控
  • 数据分析不再难:Miniconda-Python3.10镜像环境配置手把手教学
  • 从零配置SBC:用开源Kamailio搭建企业级VoIP安全网关的全流程指南
  • HPM6E00 PWM V2故障保护功能详解:16个IO触发源如何配置?
  • 域随机化:如何让AI模型在仿真中“见多识广”,在现实中“游刃有余”
  • 开源教育资源项目:打破教育信息获取壁垒,推动教育普及
  • Z-Image-Turbo-rinaiqiao-huiyewunv效果展示:辉夜大小姐Q版/写实/厚涂三种风格迁移生成效果对比
  • Windows 11终极IPX游戏联机指南:IPXWrapper完整配置教程
  • 采用STC89C54RD的智能家居控制系统设计
  • Navicat无限重置终极指南:三步搞定Mac版试用期恢复
  • 安路TangDynasty与Modelsim联合仿真实战:从模型生成到波形调试
  • 2026年4月优质的冲压件生产厂家推荐,汽车配件/金属配件/航空模具/冲压件/冲压制品/光伏连接件,冲压件产品找哪家 - 品牌推荐师
  • Vue3与BPMN.js深度整合:从零构建可视化流程设计器
  • TSIC温度传感器Arduino库:ZACwire中断解码与多传感器管理
  • RAG当主力,MemPalace把记忆准确率干到 96.6%,token 成本为0
  • 5分钟掌握抖音评论采集的完整教程:零代码数据分析利器
  • ANARCI:3步掌握抗体序列编号,让抗体研究从此标准化
  • 20260412 之所思 - 人生如梦