乘积最大子数组

LeetCode 152. 乘积最大子数组 —— 动态规划详解(含正负状态)

问题描述

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

示例 1:

输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

示例 2:

输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是连续子数组。

为什么需要两个状态?

与“最大子数组和”问题不同,乘积具有一个特殊的性质:负负得正。如果只记录到当前位置的最大乘积,当遇到负数时,可能会丢失潜在的最大值(因为一个很小的负数乘以另一个负数可能变成很大的正数)。

例如:nums = [-2, 3, -4]

  • 如果只记录最大值:
    • i=0:最大值 = -2
    • i=1:max(-2*3=-6, 3) = 3
    • i=2:max(3(-4)=-12, -4) = -4 → 最终结果为 -4,但实际最大乘积是 (-2)3*(-4)=24。

因此,我们需要同时记录以当前位置结尾的乘积的最大值和最小值,因为最小值(负数)可能在遇到负数时变成最大值。

动态规划思路

定义两个状态:

  • dpmax[i]:表示以第 i 个元素结尾的乘积最大的连续子数组的乘积。
  • dpmin[i]:表示以第 i 个元素结尾的乘积最小的连续子数组的乘积。

状态转移方程(考虑当前元素 nums[i] 可以单独成段,也可以与前面的子数组合并):

dpmax[i] = max(nums[i], dpmax[i-1] * nums[i], dpmin[i-1] * nums[i])
dpmin[i] = min(nums[i], dpmax[i-1] * nums[i], dpmin[i-1] * nums[i])

由于每一步只依赖于前一步的状态,我们可以用两个变量滚动更新,将空间复杂度降为 O(1)。但需要注意更新顺序:在计算 dpmin 时,dpmax 已经被更新了,所以需要先保存旧的 dpmax

代码实现

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0:
            return 0
        # 初始化
        dpmax = dpmin = res = nums[0]

        for i in range(1, n):
            # 保存旧的 dpmax,因为下面会立即更新
            t = dpmax
            dpmax = max(nums[i], dpmax * nums[i], dpmin * nums[i])
            dpmin = min(nums[i], dpmin * nums[i], t * nums[i])
            res = max(res, dpmax)
        return res

代码解释

  • 第 4 行:处理空数组的情况(虽然题目保证至少有一个数,但为了健壮性可以加上)。
  • 第 6 行:初始化 dpmaxdpmin 和全局结果 res 为第一个元素。
  • 循环:从第二个元素开始遍历。
    • 第 9 行t = dpmax 保存旧的 dpmax 值,因为下一行 dpmax 会被覆盖,而 dpmin 的计算需要用到旧的 dpmax(即 t)。
    • 第 10 行:更新 dpmax,考虑三种情况:当前元素单独成段、当前元素乘以上一个最大乘积、当前元素乘以上一个最小乘积(可能负负得正)。
    • 第 11 行:更新 dpmin,同样考虑三种情况,但使用旧的 dpmax(即 t)而不是刚更新的 dpmax,否则会导致逻辑错误(例如连续两个负数会互相影响)。
    • 第 12 行:更新全局最大值。

为什么更新 dpmin 时要使用旧的 dpmax

假设我们不用 t,而是直接写:

dpmax = max(nums[i], dpmax * nums[i], dpmin * nums[i])
dpmin = min(nums[i], dpmax * nums[i], dpmin * nums[i])

此时第二行中的 dpmax 已经是更新后的值,这会导致 dpmin 错误地使用了新的 dpmax 去乘 nums[i],破坏了状态转移的正确性。因为状态转移方程要求使用的是前一个位置dpmaxdpmin,而不是当前更新后的值。

复杂度分析

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

测试用例验证

  1. [2,3,-2,4] → 正确结果 6
  2. [-2,0,-1] → 正确结果 0
  3. [-2,3,-4] → 正确结果 24
  4. [0,2] → 正确结果 2
  5. [-3,-1,-1] → 正确结果 3(子数组 [-3,-1][-1,-1]

总结

这道题的关键在于认识到乘积的正负转换特性,因此需要同时维护最大值和最小值两个状态。通过滚动变量优化空间,并注意保存旧值,就能写出简洁高效的解法。这种技巧也常见于其他需要考虑正负状态的动态规划问题中(如“最大子数组绝对值”等)。

希望这篇博客能帮助你彻底理解乘积最大子数组问题!如果有任何疑问,欢迎留言讨论。

posted @ 2026-03-07 14:23  Leon_LL  阅读(0)  评论(0)    收藏  举报