目录

题目描述

152. 乘积最大子数组

image-20250419050941056

题意分析

给定整数数组 nums,找出其中乘积最大的非空连续子数组,返回该乘积。子数组必须连续,且至少包含一个元素。

如果把「乘积」换成「和」,只维护一个「以当前位置结尾的最大值」就够了。但乘法有两个特殊之处,让这条路走不通:一是负负得正——当前一个很小的负数乘积,再乘一个负数可能瞬间翻身成最大值,只盯着「最大」会把这种翻盘机会提前丢掉;二是 0 会截断——任何乘积碰到 0 都归零,0 之后必须从头再来。

数据范围上,题目保证任何前缀或子数组的乘积都在 32 位整数范围内,所以不需要考虑溢出。

边界方面:数组非空但可能只有一个元素(且可能是负数或 0),答案至少是数组中的某个单元素,因此答案的初始值必须取自数组本身,不能凭空写 0 或 1。

解法:同时维护最大和最小乘积

核心思路

问题关键:乘法遇到负数会让大小关系反转。只记录「以当前位置结尾的最大乘积」会丢掉一个很小的负数,而它可能在后面乘负数后变成最大值。

为什么选动态规划:每个连续子数组都可以看成「以前一位结尾的子数组再接上当前数」,因此只需保留上一位的结果,不必枚举左右端点。

状态与不变量:处理完 nums[i] 后,maxProdminProd 分别是所有以 i 结尾的非空子数组中的最大、最小乘积。加入 num 时只有三种候选:从 num 重新开始,或把 num 接到旧的最大、最小乘积后面,因此转移为:

  • newMax = max(num, oldMax * num, oldMin * num)
  • newMin = min(num, oldMax * num, oldMin * num)

三种可能被完整覆盖,所以状态始终成立;答案取所有结尾位置的 maxProd 最大值。候选中保留 num,也让 0 后的状态自然重新开始,无需额外分段。

解题步骤

  • nums[0] 初始化 maxProdminProdans,保证单元素、全负数组也正确。
  • 从下标 1 开始遍历,先保存 oldMaxoldMin,避免更新一个状态后污染另一个状态。
  • 用当前数与两个旧状态计算新的最大、最小乘积;裸的 num 表示从当前位置重新开始。
  • 用新的 maxProd 更新全局答案。遍历结束后返回 ans

例如处理 [-2, 3, -4] 时,到 3 为止最小乘积是 -6;再遇到 -4,它翻转成最大候选 24。这正是必须保留 minProd 的原因。

代码实现

class Solution {
    public int maxProduct(int[] nums) {
        int maxProd = nums[0];
        int minProd = nums[0];
        int ans = nums[0];

        for (int i = 1; i < nums.length; i++) {
            int num = nums[i];
            int oldMax = maxProd;
            int oldMin = minProd;

            // 负数会让最大乘积和最小乘积角色互换。
            maxProd = Math.max(num, Math.max(oldMax * num, oldMin * num));
            minProd = Math.min(num, Math.min(oldMax * num, oldMin * num));
            ans = Math.max(ans, maxProd);
        }

        return ans;
    }
}
func maxProduct(nums []int) int {
    maxProd := nums[0]
    minProd := nums[0]
    ans := nums[0]

    for i := 1; i < len(nums); i++ {
        num := nums[i]
        oldMax := maxProd
        oldMin := minProd

        // 当前数为负时,旧最小值可能变成新的最大值。
        maxProd = maxInt(num, maxInt(oldMax*num, oldMin*num))
        minProd = minInt(num, minInt(oldMax*num, oldMin*num))
        ans = maxInt(ans, maxProd)
    }

    return ans
}

func maxInt(a int, b int) int {
    if a > b {
        return a
    }
    return b
}

func minInt(a int, b int) int {
    if a < b {
        return a
    }
    return b
}

复杂度分析

  • 时间复杂度:$O(n)$,数组只遍历一次。
  • 空间复杂度:$O(1)$,只维护两个滚动状态和答案。

关键点总结

  • 面试时先指出「负数会交换最大与最小」,再自然推出双状态,这是本题主线。
  • 状态限定为「以当前位置结尾」,才能从前一位做常数时间转移。
  • 裸的 num 同时承担重新起点和处理 0 的作用。
  • 两个新状态都依赖上一轮值,更新前必须保留旧状态。

易错点总结

  • 只维护最大乘积会漏掉负负得正:[-2,3,-4] 的正确答案是 24,不是 3
  • ans 不能初始化为 0;单元素 [-2] 的答案是 -2
  • 转移不能漏掉裸的 num,否则 [0,2] 无法在 0 后重新开始。
  • 不能直接用更新后的 maxProd 计算 minProd,两者都必须基于上一轮状态。

相似题目

题目 难度 考察点
53. 最大子数组和 中等 加法版本,单一最大状态即可,本题的前置题
1567. 乘积为正数的最长子数组长度 中等 同样双状态,但维护的是正/负乘积的长度而非值
1186. 删除一次得到子数组最大和 中等 加法上叠加「删或不删」维度的双状态 DP