LeetCode算法日记 - Day 83: 乘积最大的子数组
目录
1. 乘积最大的子数组
1.1 题目解析
1.2 解法
1.3 代码实现
1. 乘积最大的子数组
https://leetcode.cn/problems/maximum-product-subarray/description/
给你一个整数数组 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 * 104-10 <= nums[i] <= 10nums的任何子数组的乘积都 保证 是一个 32-位 整数
1.1 题目解析
题目本质
这是一个"动态连续子数组最值"问题,核心是在所有可能的连续子数组中,找出乘积最大的那一个。关键词是"连续"和"乘积最大"。
常规解法
最直观的想法是枚举所有可能的连续子数组:双层循环遍历所有起点和终点,计算每个子数组的乘积,记录最大值。
class Solution {public int maxProduct(int[] nums) {int n = nums.length;int maxProduct = nums[0]; // 至少包含一个元素,初始化为第一个元素// 外层循环:枚举子数组起点for (int i = 0; i < n; i++) {int product = 1; // 当前子数组的乘积// 内层循环:枚举子数组终点for (int j = i; j < n; j++) {product *= nums[j]; // 累乘当前元素maxProduct = Math.max(maxProduct, product); // 更新最大值}}return maxProduct;}
}
问题分析
暴力枚举的时间复杂度是 O(n³)(双层循环 + 乘积计算),或优化后 O(n²)。对于 n = 2×10⁴ 的数据规模,这会超时。我们需要 O(n) 的解法。
思路转折
乘法和加法不同的关键点:
-
加法中,最大子数组和只需关注"要不要继续累加"
-
乘法中,负数×负数=正数,这意味着当前的最小值(负数)可能在下一步变成最大值
因此传统的单一 DP 状态不够用了。要想高效求解 → 必须同时追踪"最大值"和"最小值" → 引入双状态动态规划。
当遇到负数时,最小值可能翻盘成最大值;当遇到正数时,最大值继续扩大。这样才能捕获所有可能的最大乘积。
1.2 解法
解法
算法思想
使用动态规划,维护两个状态数组:
-
max[i]:以 nums[i-1] 结尾的子数组的最大乘积
-
min[i]:以 nums[i-1] 结尾的子数组的最小乘积
状态转移方程:
max[i] = max(nums[i-1], nums[i-1] × max[i-1], nums[i-1] × min[i-1])min[i] = min(nums[i-1], nums[i-1] × max[i-1], nums[i-1] × min[i-1])result = max(result, max[i])
i)创建大小为 n+1 的
ii)初始化 max[0] = min[0] = 1,作为虚拟起点,方便处理第一个元素
iii)初始化 result = Integer.MIN_VALUE,记录全局最大值
iv)从 i=1 遍历到 i=n,对每个位置:
-
计算 max[i]:从三个候选值中取最大(单独取当前元素、继承上一个最大值、继承上一个最小值)
-
计算 min[i]:从三个候选值中取最小(单独取当前元素、继承上一个最大值、继承上一个最小值)
-
用 max[i] 更新全局最大值 result
v)返回 (int)result
易错点
-
只维护最大值:会漏掉"最小负数×负数=最大正数"的情况,例如 [-2, -3] 应该返回 6,但只维护最大值会返回 -2
-
result 每次被覆盖:如果写成 result = Math.max(max[i], min[i]),每次循环都会覆盖 result,丢失历史最大值。必须写成 result = Math.max(result, max[i]) 来累积保存所有位置中的最大值
1.3 代码实现
class Solution {public int maxProduct(int[] nums) {int n = nums.length;long[] max = new long[n+1];long[] min = new long[n+1];max[0] = min[0] = 1;long result = Integer.MIN_VALUE;for(int i = 1; i <= n; i++){max[i] = Math.max(nums[i-1], Math.max(nums[i-1]*max[i-1], nums[i-1]*min[i-1]));min[i] = Math.min(nums[i-1], Math.min(nums[i-1]*max[i-1], nums[i-1]*min[i-1]));result = Math.max(result, max[i]);} return (int)result;}
}
复杂度分析
-
时间复杂度:O(n),只需遍历数组一次,每个位置的计算都是 O(1)
-
空间复杂度:O(n),使用了两个长度为 n+1 的数组存储中间状态
