题目:线性动态规划:乘积最大子数组
字数 3183 2025-12-08 04:57:39

好的,我为你介绍一个线性动态规划领域里非常经典且实用的题目。根据你的要求,它不在你给出的已讲题目列表中。

题目:线性动态规划:乘积最大子数组


题目描述

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

示例 1:

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

示例 2:

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

注意: 测试用例的答案是一个 32位 整数。数组中的元素可以是负数、零和正数。乘积可能会非常大(包括负数),所以简单的最大值递推会失效,因为我们不仅要记录最大乘积,还需要记录可能因后续遇到负数而“翻身”的最小乘积(负得最多的那个数)。


解题过程循序渐进讲解

第一步:理解问题的核心矛盾与关键观察

这个问题的核心在于负数的存在以及连续性

  1. 连续性:我们要求的是连续子数组,而不是可以跳跃选取的子序列。
  2. 负数的影响
    • 正数乘以正数,结果变大。
    • 负数乘以负数,结果会“负负得正”,变得更大。
    • 零会“重置”所有累积的乘积。

这个特性意味着,对于以当前元素 nums[i] 结尾的子数组,其最大乘积不仅取决于上一个状态的最大乘积 maxProd[i-1],还取决于上一个状态的最小乘积 minProd[i-1](因为当前元素可能是负数)。

关键观察:对于以位置 i 结尾的子数组,其最大乘积可能来自以下三种情况之一:

  1. nums[i] 自己(例如,前面是0,或者当前数非常大)。
  2. nums[i] * maxProd[i-1](例如,nums[i] 是正数,乘上之前的最大正数积)。
  3. nums[i] * minProd[i-1](例如,nums[i] 是负数,乘上之前“负得最多”的积,反而能得到一个大的正数)。

同样,以位置 i 结尾的子数组,其最小乘积也可能来自:

  1. nums[i] 自己。
  2. nums[i] * maxProd[i-1](例如,nums[i] 是负数,乘上一个大正数,会变成一个大负数)。
  3. nums[i] * minProd[i-1](例如,nums[i] 是正数,乘上一个很小的负数,还是一个很小的负数)。

第二步:定义动态规划状态

基于以上分析,我们需要两个动态规划数组(或变量)来同时追踪状态:

  • maxDP[i]:表示以第 i 个元素结尾的连续子数组的最大乘积
  • minDP[i]:表示以第 i 个元素结尾的连续子数组的最小乘积(通常是负数,但也是乘积最大的负数)。

我们的最终答案是所有 maxDP[i] 中的最大值。

第三步:推导状态转移方程

对于每个位置 ii >= 1),我们需要用 maxDP[i-1]minDP[i-1]nums[i] 来更新 maxDP[i]minDP[i]

状态转移方程如下:

  1. 更新 maxDP[i]:它应该是下面三个值的最大值。

    • nums[i]: 子数组只包含它自己。
    • nums[i] * maxDP[i-1]: 当前数乘以前面的最大乘积。
    • nums[i] * minDP[i-1]: 当前数乘以前面的最小乘积(负负得正)。

    所以:maxDP[i] = max(nums[i], nums[i] * maxDP[i-1], nums[i] * minDP[i-1])

  2. 更新 minDP[i]:它应该是下面三个值的最小值(用于未来可能遇到负数时“翻身”)。

    • nums[i]
    • nums[i] * maxDP[i-1](正数乘大负数,或负数乘大正数,都可能导致很小的负数)
    • nums[i] * minDP[i-1]

    所以:minDP[i] = min(nums[i], nums[i] * maxDP[i-1], nums[i] * minDP[i-1])

初始化

  • i = 0 时,以第一个元素结尾的子数组只有它自己。所以:
    • maxDP[0] = nums[0]
    • minDP[0] = nums[0]
    • 全局答案 ans 初始化为 nums[0]

第四步:逐步演算示例

让我们用示例 nums = [2, 3, -2, 4] 来走一遍流程。

下标 i nums[i] 计算过程 maxDP[i] minDP[i] 更新全局答案 ans
0 2 初始化 2 2 2
1 3 max(3, 3*2, 3*2) = max(3, 6, 6) = 6
min(3, 3*2, 3*2) = min(3, 6, 6) = 3
6 3 max(2, 6) = 6
2 -2 max(-2, -2*6, -2*3) = max(-2, -12, -6) = -2
min(-2, -2*6, -2*3) = min(-2, -12, -6) = -12
-2 -12 max(6, -2) = 6
3 4 max(4, 4*(-2), 4*(-12)) = max(4, -8, -48) = 4
min(4, 4*(-2), 4*(-12)) = min(4, -8, -48) = -48
4 -48 max(6, 4) = 6

最终结果:全局答案 ans = 6,对应子数组 [2, 3]

第五步:空间优化(可选但推荐)

观察状态转移方程,maxDP[i]minDP[i] 只依赖于前一个状态 maxDP[i-1]minDP[i-1]。因此,我们不需要维护整个数组,只需要两个变量来滚动记录前一个状态即可。

优化后的步骤

  1. 初始化:
    • curMax = nums[0]
    • curMin = nums[0]
    • ans = nums[0]
  2. i = 1 开始遍历数组:
    • 由于计算 curMaxcurMin 需要用到旧的 curMaxcurMin,我们必须先把它们暂存起来,否则更新 curMax 后,计算 curMin 用的就是新的错误值。
    • tempMax = curMax, tempMin = curMin
    • 更新:
      • curMax = max(nums[i], nums[i] * tempMax, nums[i] * tempMin)
      • curMin = min(nums[i], nums[i] * tempMax, nums[i] * tempMin)
    • 更新全局答案:ans = max(ans, curMax)

第六步:代码实现(Python示例)

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        # 初始化
        cur_max = cur_min = ans = nums[0]
        
        # 从第二个元素开始遍历
        for i in range(1, len(nums)):
            num = nums[i]
            # 暂存旧值
            temp_max, temp_min = cur_max, cur_min
            
            # 更新当前最大值和最小值
            cur_max = max(num, num * temp_max, num * temp_min)
            cur_min = min(num, num * temp_max, num * temp_min)
            
            # 更新全局最大乘积
            ans = max(ans, cur_max)
        
        return ans

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。我们只遍历了一次数组。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

总结

这道题通过引入两个互补的状态(maxDPminDP),巧妙地解决了负数对连续子数组乘积的影响问题。其核心思想是:当前位置的最优解可能由历史最优解、历史最差解和当前值共同决定。这是动态规划中一种非常典型的状态设计思路,尤其在处理具有“符号翻转”特性的最优化问题时非常有效。

好的,我为你介绍一个线性动态规划领域里非常经典且实用的题目。根据你的要求,它不在你给出的已讲题目列表中。 题目:线性动态规划:乘积最大子数组 题目描述 给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组至少包含一个数字),并返回该子数组所对应的乘积。 示例 1: 示例 2: 注意: 测试用例的答案是一个 32位 整数。数组中的元素可以是负数、零和正数。乘积可能会非常大(包括负数),所以简单的最大值递推会失效,因为我们不仅要记录最大乘积,还需要记录可能因后续遇到负数而“翻身”的最小乘积(负得最多的那个数)。 解题过程循序渐进讲解 第一步:理解问题的核心矛盾与关键观察 这个问题的核心在于 负数的存在 以及 连续性 。 连续性 :我们要求的是连续子数组,而不是可以跳跃选取的子序列。 负数的影响 : 正数乘以正数,结果变大。 负数乘以负数,结果会“负负得正”,变得更大。 零会“重置”所有累积的乘积。 这个特性意味着,对于以当前元素 nums[i] 结尾的子数组,其最大乘积不仅取决于上一个状态的最大乘积 maxProd[i-1] ,还取决于上一个状态的最小乘积 minProd[i-1] (因为当前元素可能是负数)。 关键观察 :对于以位置 i 结尾的子数组,其最大乘积可能来自以下三种情况之一: nums[i] 自己(例如,前面是0,或者当前数非常大)。 nums[i] * maxProd[i-1] (例如, nums[i] 是正数,乘上之前的最大正数积)。 nums[i] * minProd[i-1] (例如, nums[i] 是负数,乘上之前“负得最多”的积,反而能得到一个大的正数)。 同样,以位置 i 结尾的子数组,其最小乘积也可能来自: nums[i] 自己。 nums[i] * maxProd[i-1] (例如, nums[i] 是负数,乘上一个大正数,会变成一个大负数)。 nums[i] * minProd[i-1] (例如, nums[i] 是正数,乘上一个很小的负数,还是一个很小的负数)。 第二步:定义动态规划状态 基于以上分析,我们需要两个动态规划数组(或变量)来同时追踪状态: maxDP[i] :表示以第 i 个元素结尾的连续子数组的 最大乘积 。 minDP[i] :表示以第 i 个元素结尾的连续子数组的 最小乘积 (通常是负数,但也是乘积最大的负数)。 我们的最终答案是所有 maxDP[i] 中的最大值。 第三步:推导状态转移方程 对于每个位置 i ( i >= 1 ),我们需要用 maxDP[i-1] 、 minDP[i-1] 和 nums[i] 来更新 maxDP[i] 和 minDP[i] 。 状态转移方程如下: 更新 maxDP[i] :它应该是下面三个值的最大值。 nums[i] : 子数组只包含它自己。 nums[i] * maxDP[i-1] : 当前数乘以前面的最大乘积。 nums[i] * minDP[i-1] : 当前数乘以前面的最小乘积(负负得正)。 所以: maxDP[i] = max(nums[i], nums[i] * maxDP[i-1], nums[i] * minDP[i-1]) 更新 minDP[i] :它应该是下面三个值的最小值(用于未来可能遇到负数时“翻身”)。 nums[i] nums[i] * maxDP[i-1] (正数乘大负数,或负数乘大正数,都可能导致很小的负数) nums[i] * minDP[i-1] 所以: minDP[i] = min(nums[i], nums[i] * maxDP[i-1], nums[i] * minDP[i-1]) 初始化 : 当 i = 0 时,以第一个元素结尾的子数组只有它自己。所以: maxDP[0] = nums[0] minDP[0] = nums[0] 全局答案 ans 初始化为 nums[0] 第四步:逐步演算示例 让我们用示例 nums = [2, 3, -2, 4] 来走一遍流程。 | 下标 i | nums[i] | 计算过程 | maxDP[i] | minDP[i] | 更新全局答案 ans | | :------- | :-------- | :------- | :--------- | :--------- | :---------------- | | 0 | 2 | 初始化 | 2 | 2 | 2 | | 1 | 3 | max(3, 3*2, 3*2) = max(3, 6, 6) = 6 min(3, 3*2, 3*2) = min(3, 6, 6) = 3 | 6 | 3 | max(2, 6) = 6 | | 2 | -2 | max(-2, -2*6, -2*3) = max(-2, -12, -6) = -2 min(-2, -2*6, -2*3) = min(-2, -12, -6) = -12 | -2 | -12 | max(6, -2) = 6 | | 3 | 4 | max(4, 4*(-2), 4*(-12)) = max(4, -8, -48) = 4 min(4, 4*(-2), 4*(-12)) = min(4, -8, -48) = -48 | 4 | -48 | max(6, 4) = 6 | 最终结果 :全局答案 ans = 6 ,对应子数组 [2, 3] 。 第五步:空间优化(可选但推荐) 观察状态转移方程, maxDP[i] 和 minDP[i] 只依赖于前一个状态 maxDP[i-1] 和 minDP[i-1] 。因此,我们不需要维护整个数组,只需要两个变量来滚动记录前一个状态即可。 优化后的步骤 : 初始化: curMax = nums[0] curMin = nums[0] ans = nums[0] 从 i = 1 开始遍历数组: 由于计算 curMax 和 curMin 需要用到 旧的 curMax 和 curMin ,我们必须先把它们暂存起来,否则更新 curMax 后,计算 curMin 用的就是新的错误值。 令 tempMax = curMax , tempMin = curMin 。 更新: curMax = max(nums[i], nums[i] * tempMax, nums[i] * tempMin) curMin = min(nums[i], nums[i] * tempMax, nums[i] * tempMin) 更新全局答案: ans = max(ans, curMax) 第六步:代码实现(Python示例) 复杂度分析 : 时间复杂度 :O(n),其中 n 是数组的长度。我们只遍历了一次数组。 空间复杂度 :O(1),只使用了常数级别的额外空间。 总结 这道题通过引入两个互补的状态( maxDP 和 minDP ),巧妙地解决了负数对连续子数组乘积的影响问题。其核心思想是: 当前位置的最优解可能由历史最优解、历史最差解和当前值共同决定 。这是动态规划中一种非常典型的状态设计思路,尤其在处理具有“符号翻转”特性的最优化问题时非常有效。