从和到积:破解最大子数组乘积的双状态 DP 之道

59 阅读4分钟

乘积最大子数组:动态规划解法详解

在算法面试中,“最大子数组和”是一个经典问题(LeetCode #53),但当问题变为 “最大子数组乘积” (LeetCode #152)时,难度陡然上升。原因在于:乘法的符号特性使得负数可能“翻身”成为最大值。本文将深入剖析这一问题,并通过动态规划给出高效解法。

问题描述

152. 乘积最大子数组 - 力扣(LeetCode)
给定一个整数数组 nums,找出一个连续子数组,使其元素的乘积最大。返回该最大乘积。

例如:

  • 输入:[2, 3, -2, 4] → 输出:6(子数组 [2,3]
  • 输入:[-2, 0, -1] → 输出:0
  • 输入:[-2, 3, -4] → 输出:24(整个数组)

为什么不能直接套用“最大子数组和”的思路?

在“最大子数组和”中,我们只需维护一个 dp[i] = max(dp[i-1] + nums[i], nums[i])
但在乘积问题中,负数 × 负数 = 正数,这意味着:

当前最小的负数乘积,遇到另一个负数,可能变成最大的正数乘积!

因此,仅记录“到当前位置的最大乘积”是不够的——我们还需要同时跟踪最小乘积

动态规划状态设计

我们定义两个状态变量(无需完整 DP 数组,只需滚动变量):

  • premax:以当前元素结尾的子数组所能得到的最大乘积
  • premin:以当前元素结尾的子数组所能得到的最小乘积

状态转移方程

对于每个新元素 nums[i],我们有三种选择:

  1. nums[i] 接在之前的子数组后面(乘上 premaxpremin
  2. nums[i] 重新开始一个新的子数组

因此:

prevmin=min(prevmin∗nums[i],prevmax∗nums[i],nums[i])
prevmax=max(prevmin∗nums[i],prevmax∗nums[i],nums[i])

⚠️ 注意:观察发现,左边的代表第i项的乘积,右边的premin代表第i-1项的乘积。当我们迭代后的premin已经改变,所以当我们想要更新premax时,premin显然已经不是第i-1项的最小积了。 因此我们需要两个变量来暂时保存最大和最小积。

    t1=premin*nums[i];
    t2=premax*nums[i];
    premin=Math.min(t1,t2,nums[i]);
    premax=Math.max(t1,t2,nums[i]);

完整代码实现

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxProduct = function(nums) {
    let res =nums[0];
    let premin=nums[0];
    let premax =nums[0];
    let t1=0,t2=0;
    for(let i=1;i<nums.length;i++){
        t1=premin*nums[i];
        t2=premax*nums[i];
        premin=Math.min(t1,t2,nums[i]);
        premax=Math.max(t1,t2,nums[i]);
        //记录当前最大值,避免覆盖后丢失
        res=Math.max(premax,res);
    }
    return res;
};

关键点解析

1. 为什么要考虑 nums[i] 自身?

当之前的乘积为 0 或负数,而 nums[i] 是一个较大的正数时,重新开始子数组更优
例如:[0, 5] → 最大乘积是 5,而不是 0*5=0

同样,若 nums[i] 是一个极小的负数(如 -100),而之前乘积接近 0,那么 premin 会取 -100 本身。

2. 零的影响

零会“重置”状态:

  • preminpremax 都会变成 0
  • 下一个非零元素将从自身重新开始计算

这正是算法能正确处理 [2, 0, 3] → 返回 3 的原因。

3. 负负得正的处理

[-2, 3, -4] 为例:

  • i=0: premin=-2, premax=-2
  • i=1: t1=-6, t2=-6 → premin=-6, premax=3(选了自身!)
  • i=2: t1=(-6) (-4)=24, t2=3(-4)=-12 → premax=24 ✅

完美捕捉到负负得正的最大值。

时间与空间复杂度

  • 时间复杂度:O(n),仅遍历一次数组
  • 空间复杂度:O(1),只使用常数个变量

总结

“最大乘积子数组”看似只是将“和”换成“积”,实则揭示了动态规划中一个深刻原则:状态设计必须覆盖所有可能影响未来决策的历史信息

在加法中,历史只需一个“最大和”;但在乘法中,最小值可能在未来因符号反转而成为最大值。因此,我们必须同时维护 premaxpremin —— 这不是技巧堆砌,而是对问题结构的忠实建模。

此外,算法始终保留 nums[i] 作为候选,体现了动态规划的另一核心思想:局部最优解不一定延续历史,有时“重新开始”才是全局最优的起点

最终,这段简洁的代码之所以正确,正是因为它:

  • 完整枚举了所有可能的转移路径(接最大、接最小、从头开始),
  • 严谨避免了状态覆盖错误
  • 实时追踪全局最优解

掌握这种“双状态 + 重置选项”的模式,不仅能解决本题,也为处理其他涉及符号、波动或非单调运算的序列问题提供了通用思路。