2026/3/24 7:03:23
网站建设
项目流程
网站的建站方案,网站排名seo教程,建设大型网站,在哪个网站上做推广作用好Java版LeetCode热题100之乘积最大子数组#xff1a;动态规划中的正负博弈与空间优化艺术 本文深入剖析 LeetCode 第152题「乘积最大子数组」#xff0c;从问题本质出发#xff0c;详解为何普通最大子序和思路失效#xff0c;如何通过维护最大/最小值双状态解决负数翻转问题…Java版LeetCode热题100之乘积最大子数组动态规划中的正负博弈与空间优化艺术本文深入剖析 LeetCode 第152题「乘积最大子数组」从问题本质出发详解为何普通最大子序和思路失效如何通过维护最大/最小值双状态解决负数翻转问题并涵盖代码实现、复杂度分析、面试高频问题、实际应用场景及延伸思考是一篇面向中高级开发者的高质量技术博客。一、原题回顾题目编号LeetCode 152题目名称乘积最大子数组Maximum Product Subarray难度等级中等Medium题目描述给你一个整数数组nums请你找出数组中乘积最大的非空连续子数组该子数组中至少包含一个数字并返回该子数组所对应的乘积。测试用例的答案是一个32位整数一个只包含一个元素的数组的乘积是这个元素的值。示例示例 1 输入: nums [2,3,-2,4] 输出: 6 解释: 子数组 [2,3] 有最大乘积 6。 示例 2 输入: nums [-2,0,-1] 输出: 0 解释: 结果不能为 2因为 [-2,-1] 不是连续子数组。约束条件1 nums.length 2 * 10⁴-10 nums[i] 10nums的任何子数组的乘积都保证是一个 32 位整数二、原题分析这道题看似是「最大子序和」LeetCode 53的乘法版本但本质完全不同。2.1 为什么不能直接套用最大子序和的思路在最大子序和中我们定义dp[i] max(dp[i-1] nums[i], nums[i])因为加法具有单调性更大的前缀和总能带来更大的当前和。但乘法不具备这种单调性原因在于负数的存在两个负数相乘得正数零的存在任何数乘以 0 得 0会“截断”序列正负翻转当前最大值可能由之前的最小值负数乘以当前负数得到。2.2 关键洞察负负得正考虑数组[5, 6, -3, 4, -3]前两数乘积30最大全体乘积5×6×(-3)×4×(-3) 1080更大问题出在哪里当我们遇到第二个-3时之前累积的最小值即最负的值是5×6×(-3)×4 -360乘以-3后变为1080远超之前的30。因此仅维护“以 i 结尾的最大乘积”是不够的还必须维护“以 i 结尾的最小乘积”。三、答案构思动态规划双状态3.1 状态定义我们定义两个状态maxF[i]以nums[i]结尾的最大子数组乘积minF[i]以nums[i]结尾的最小子数组乘积。注意nums[i]必须被包含。3.2 状态转移方程对于每个位置i我们有三种选择只取nums[i]将nums[i]接到以i-1结尾的最大乘积后maxF[i-1] * nums[i]将nums[i]接到以i-1结尾的最小乘积后minF[i-1] * nums[i]。由于nums[i]可能为正、负或零我们需要同时考虑最大和最小maxF [ i ] max ( maxF [ i − 1 ] × nums [ i ] , minF [ i − 1 ] × nums [ i ] , nums [ i ] ) minF [ i ] min ( maxF [ i − 1 ] × nums [ i ] , minF [ i − 1 ] × nums [ i ] , nums [ i ] ) \begin{aligned} \text{maxF}[i] \max(\text{maxF}[i-1] \times \text{nums}[i],\ \text{minF}[i-1] \times \text{nums}[i],\ \text{nums}[i]) \\ \text{minF}[i] \min(\text{maxF}[i-1] \times \text{nums}[i],\ \text{minF}[i-1] \times \text{nums}[i],\ \text{nums}[i]) \end{aligned}maxF[i]minF[i]max(maxF[i−1]×nums[i],minF[i−1]×nums[i],nums[i])min(maxF[i−1]×nums[i],minF[i−1]×nums[i],nums[i])3.3 初始条件与结果初始maxF[0] minF[0] nums[0]最终答案max(maxF[0], maxF[1], ..., maxF[n-1])四、完整答案方法一数组版 DPpublicclassSolution{publicintmaxProduct(int[]nums){if(numsnull||nums.length0){return0;}intnnums.length;// 使用 long 防止中间计算溢出虽然题目保证结果在32位内但中间可能溢出long[]maxFnewlong[n];long[]minFnewlong[n];// 初始化maxF[0]minF[0]nums[0];intresultnums[0];for(inti1;in;i){longcurrentnums[i];// 计算三种可能的值longcandidate1maxF[i-1]*current;longcandidate2minF[i-1]*current;maxF[i]Math.max(current,Math.max(candidate1,candidate2));minF[i]Math.min(current,Math.min(candidate1,candidate2));resultMath.max(result,(int)maxF[i]);}returnresult;}}代码说明使用long类型避免中间乘积溢出如10^5 * 10^5 10^10 2^31显式计算三个候选值逻辑清晰实时更新全局最大值result。五、代码分析方法一5.1 正确性验证以nums [2,3,-2,4]为例inums[i]maxF[i-1]minF[i-1]候选值maxF[i]minF[i]02---2213226,6,3632-263-12,-6,-2-2-1234-2-12-8,-48,44-48全局最大值max(2,6,-2,4) 6✅再看nums [-2,0,-1]inums[i]maxFminF0-2-2-210max(0, 0, 0)0min(0,0,0)02-1max(-1, 0, 0)-1? → 实际max(-1, 0*(-1)0, 0*(-1)0) 0min(-1,0,0)-1全局最大值max(-2,0,-1) 0✅5.2 边界处理单元素数组直接返回该元素全零数组返回 0全负数组返回最大即绝对值最小的负数包含零零会重置状态相当于分割数组。六、时间复杂度与空间复杂度分析方法一时间复杂度O(n)单次遍历数组每次进行常数次比较和乘法总计O(n)空间复杂度O(n)两个长度为 n 的long数组总计O(n)对于n20000占用约 320KB 内存2 × 20000 × 8 bytes可接受但可进一步优化。七、答案构思方法二 —— 空间优化滚动变量7.1 优化思想观察状态转移方程maxF[i]和minF[i]仅依赖于maxF[i-1]和minF[i-1]因此无需存储整个数组只需两个变量保存前一状态。这就是经典的滚动数组Rolling Array优化。7.2 注意事项在更新maxF和minF时必须先保存旧值否则maxF更新后会影响minF的计算。例如// 错误写法maxFMath.max(...,minF*nums[i]);// 此时 minF 还是旧的minFMath.min(...,maxF*nums[i]);// 但 maxF 已被更新正确做法先缓存prevMax maxF,prevMin minF。八、完整答案方法二空间优化版publicclassSolution{publicintmaxProduct(int[]nums){if(numsnull||nums.length0){return0;}// 使用 long 防止中间溢出longmaxFnums[0];// 当前最大乘积longminFnums[0];// 当前最小乘积intresultnums[0];for(inti1;inums.length;i){longcurrentnums[i];// 保存上一轮的状态longprevMaxmaxF;longprevMinminF;// 更新当前状态maxFMath.max(current,Math.max(prevMax*current,prevMin*current));minFMath.min(current,Math.min(prevMax*current,prevMin*current));// 更新全局最大值resultMath.max(result,(int)maxF);}returnresult;}}代码优势空间复杂度降至 O(1)逻辑清晰无冗余存储性能更优尤其在大数据量时。九、代码分析方法二9.1 为何使用long虽然题目保证最终结果在 32 位整数范围内但中间计算可能溢出。例如nums [10000, 10000]中间乘积为100000000仍在 int 范围内但若nums [50000, 50000]虽然本题约束|nums[i]| ≤ 10不会发生则会溢出。使用long是一种防御性编程确保算法鲁棒性。注本题|nums[i]| ≤ 10n ≤ 20000最大乘积为10^20000远超 long 范围但题目明确说明“任何子数组的乘积都保证是 32 位整数”故中间值虽可能很大但不会导致错误比较因最终结果在 int 内且我们只关心相对大小。实际上由于存在负数和零乘积不会无限增长。但在通用场景下使用long是良好实践。9.2 关于溢出处理的讨论原题提供的参考代码中有if(minF(-131)){minFnums[i];}这是试图防止long转int时溢出。但这是不必要的因为我们只在最后将maxF转为int用于比较题目保证最终结果在 32 位整数内中间long值即使很大只要比较逻辑正确不影响结果。因此无需特殊溢出处理上述判断可删除。十、常见问题解答FAQQ1为什么需要同时维护最大值和最小值A因为负数会翻转大小关系。当前最大值可能由之前的最小值负数乘以当前负数得到。例如minF -10,nums[i] -2→product 20可能成为新的最大值。Q2如果数组中全是正数是否可以简化A可以此时minF[i] nums[i]因为累积乘积只会越来越大maxF[i] maxF[i-1] * nums[i]。但为了代码统一性通常不单独处理。Q3零如何影响状态A当nums[i] 0时maxF[i] max(0, 0, 0) 0minF[i] min(0, 0, 0) 0相当于“重置”状态后续计算从 0 开始。Q4能否只用一个变量A不能。因为最大值和最小值相互依赖且无法从单一状态推导出另一个。十一、优化思路拓展11.1 分治法理论可行实际不优将数组分为左右两半最大乘积子数组可能完全在左半完全在右半跨越中点。跨越中点的情况需计算从中点向左的最大/最小乘积从中点向右的最大/最小乘积组合四种情况左大×右大左大×右小左小×右大左小×右小。时间复杂度 O(n log n)不如 DP 的 O(n)故不推荐。11.2 处理超大数BigInteger若题目不限制结果范围需使用BigInteger但性能较差且本题无需。11.3 并行化由于状态依赖前一状态难以并行。但若只需求近似解可分段处理后合并非精确。十二、数据结构与算法基础知识点回顾12.1 动态规划的适用条件最优子结构全局最优解包含子问题最优解重叠子问题子问题被重复计算无后效性当前状态只与过去有关。本题中以i结尾的最大乘积仅依赖i-1的状态满足无后效性。12.2 滚动数组优化适用于状态仅依赖前 k 个状态的问题将空间复杂度从 O(n) 降至 O(k)常见于斐波那契、背包、本题等。12.3 负数的特殊性在涉及乘法的优化问题中负数会改变单调性导致最大/最小值互换需要同时跟踪极值。这是本题区别于“最大子序和”的核心。12.4 整数溢出处理Java 中int范围[-2³¹, 2³¹-1]乘法易溢出使用long是常用技巧但需注意long也可能溢出本题因约束安全。十三、面试官提问环节模拟对话面试官你为什么认为最大子序和的思路在这里行不通答因为加法具有单调性——更大的前缀和总能带来更大的当前和。但乘法没有负数会使大小关系反转。例如前缀积为 -10很小当前数为 -2乘积为 20很大。所以必须同时跟踪最大和最小值。面试官你的代码用了 long但题目说结果是 32 位整数有必要吗答有必要。虽然最终结果在 int 范围内但中间计算可能溢出。例如假设我们有 [100000, 100000]尽管本题约束不会出现乘积为 10¹⁰超过 int 范围。使用 long 确保中间比较正确。这是一种防御性编程。面试官如果数组中有很多零你的算法效率如何答依然 O(n)。零会使 maxF 和 minF 重置为 0但后续计算正常进行。算法天然处理了零的情况无需特殊分支。面试官能否修改算法以返回具体的子数组而不仅是乘积答可以但需要额外记录起始位置。每当 maxF 更新时记录当前结束位置和对应的起始位置可通过维护 start/end 索引实现。但会增加空间复杂度至 O(n)。十四、这道算法题在实际开发中的应用14.1 金融风控在股票收益率序列中寻找连续时间段内最大复合收益率收益率相乘。负收益率代表亏损需考虑“亏损后反弹”的情况。14.2 信号处理在音频或图像处理中某些特征值可能为负需检测能量平方或绝对值最大的连续片段但若保留符号则需类似 LIS 的乘积分析。14.3 游戏开发在 RPG 游戏中角色有增益/减益 buff如攻击力 ×1.5 或 ×0.5需计算连续战斗中最大伤害输出窗口。14.4 数据压缩在某些编码方案中数据块的“权重”为乘积形式需找到权重最大的连续块以优化存储。14.5 机器学习在概率模型中联合概率为各条件概率的乘积。若对数概率不可用需直接处理乘积此时最大概率路径可建模为本题。十五、相关题目推荐题号题目关联点53. 最大子序和加法版本对比学习1567. 乘积为正数的最长子数组长度符号分析只关心正负918. 环形子数组的最大和环形扩展更复杂结构1371. 每个元音包含偶数次的最长子字符串状态压缩异或思想238. 除自身以外数组的乘积乘积处理前后缀乘积十六、总结与延伸16.1 核心收获负数是乘法优化问题的关键难点同时维护最大/最小值是处理符号翻转的标准技巧滚动数组优化是 DP 空间优化的经典手段理解问题本质而非套模板是解决算法题的核心。16.2 延伸思考如果允许删除一个元素可分别计算删除每个位置后的最大乘积时间复杂度 O(n²)或用前后缀乘积优化至 O(n)。如果数组是环形的需考虑跨越首尾的情况类似环形子数组最大和。如果要求乘积为正的最大长度只需跟踪符号变化无需具体数值。16.3 给读者的建议不要死记硬背代码理解“为何需要 minF”多画状态转移表尤其是包含负数和零的例子面试中先写清晰的数组版 DP再优化到滚动变量重视边界测试全正、全负、含零、单元素等。结语乘积最大子数组是一道极具启发性的动态规划题。它打破了“最大子序和”的思维定式揭示了负数在乘法中的独特作用。掌握它不仅是为了通过一道面试题更是为了培养对问题本质的洞察力。希望本文能助你在算法之路上走得更远