Problem: 152. 乘积最大子数组
文章目录
整体思路
这段代码旨在解决经典的 "乘积最大子数组" (Maximum Product Subarray) 问题。问题要求在一个包含正数、负数和零的整数数组中,找到一个连续子数组,使得该子数组内所有元素的乘积最大,并返回这个最大乘积。
与"最大子数组和"不同,乘积问题因为负数的存在而变得复杂:一个当前很小的负数(最小值)乘以另一个负数,可能会变成一个很大的正数(最大值)。因此,只维护最大值是不够的。
该算法采用了一种非常巧妙的 动态规划 方法。它在每一步都同时维护以当前元素结尾的最大乘积 和最小乘积。
-
状态定义:
- 算法定义了两个DP数组:
dpMax[i]
:以nums[i]
为结尾的连续子数组的最大乘积。dpMin[i]
:以nums[i]
为结尾的连续子数组的最小乘积(这个主要是为了处理负数)。
- 算法定义了两个DP数组:
-
状态转移方程:
- 为了计算
dpMax[i]
和dpMin[i]
,我们需要考虑nums[i]
与前一个状态的关系。以nums[i]
结尾的子数组,要么只包含nums[i]
本身,要么是nums[i]
连接在以nums[i-1]
结尾的子数组后面。 - 当
nums[i]
与前面的子数组(dpMax[i-1]
和dpMin[i-1]
)相乘时,会出现以下情况:- 如果
nums[i]
是正数:dpMax[i-1] * nums[i]
可能是新的最大值,dpMin[i-1] * nums[i]
可能是新的最小值。 - 如果
nums[i]
是负数:dpMin[i-1] * nums[i]
(负负得正)可能变成新的最大值,而dpMax[i-1] * nums[i]
(正负得负)可能变成新的最小值。
- 如果
- 因此,
dpMax[i]
的候选值有三个:dpMax[i-1] * nums[i]
:前一个最大值乘以当前数。dpMin[i-1] * nums[i]
:前一个最小值乘以当前数(处理负负得正)。nums[i]
:不与前面连接,子数组只包含当前数自身。
dpMax[i]
就是这三者中的最大值。
- 同理,
dpMin[i]
的候选值也是这三个,但取的是最小值。 - 状态转移方程:
dpMax[i] = max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i])
dpMin[i] = min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i])
- 为了计算
-
最终结果:
dpMax[i]
仅代表以nums[i]
结尾的最大乘积,不一定是全局的最大乘积。- 全局的最大乘积必然是所有
dpMax[i]
中的某一个。 - 因此,在计算完整个
dpMax
数组后,需要遍历它来找到其中的最大值作为最终答案。
完整代码
java
import java.util.Arrays;
class Solution {
/**
* 找到一个具有最大乘积的连续子数组,并返回其乘积。
* @param nums 整数数组
* @return 最大乘积
*/
public int maxProduct(int[] nums) {
int n = nums.length;
// dpMax[i]: 以 nums[i] 结尾的连续子数组的最大乘积。
int[] dpMax = new int[n];
// dpMin[i]: 以 nums[i] 结尾的连续子数组的最小乘积。
int[] dpMin = new int[n];
// 基础情况:以 nums[0] 结尾的子数组只有一个,其最大和最小乘积都是 nums[0]。
dpMax[0] = dpMin[0] = nums[0];
// 从第二个元素开始,应用状态转移方程
for (int i = 1; i < n; i++) {
int x = nums[i];
// 计算 dpMax[i]:
// 它的候选值有三个:
// 1. dpMax[i-1] * x: 前一个最大值乘以当前数。
// 2. dpMin[i-1] * x: 前一个最小值乘以当前数 (处理负负得正的情况)。
// 3. x: 子数组只包含当前数自身。
dpMax[i] = Math.max(Math.max(dpMax[i - 1] * x, dpMin[i - 1] * x), x);
// 计算 dpMin[i],逻辑同上,只是取最小值。
dpMin[i] = Math.min(Math.min(dpMax[i - 1] * x, dpMin[i - 1] * x), x);
}
// 全局最大乘积是所有 dpMax[i] 中的最大值。
// 使用 stream API 来方便地找到数组中的最大值。
return Arrays.stream(dpMax).max().getAsInt();
}
}
时空复杂度
时间复杂度:O(N)
- 循环 :算法的主体是一个
for
循环,从i=1
遍历到n-1
。算上初始化,整个nums
数组被访问了一次。循环执行了N-1
次。 - 循环内部操作 :
- 在循环的每一次迭代中,执行的都是基本的乘法、
Math.max
/Math.min
和数组访问操作。这些操作的时间复杂度都是 O(1)。
- 在循环的每一次迭代中,执行的都是基本的乘法、
- 结果查找 :
Arrays.stream(dpMax).max().getAsInt()
需要遍历整个dpMax
数组一次来找到最大值。这部分的时间复杂度是 O(N)。
综合分析 :
算法的总时间复杂度由两个独立的线性扫描组成:O(N) (填充DP数组) + O(N) (查找最大值)。因此,最终的时间复杂度是 O(N)。
空间复杂度:O(N)
- 主要存储开销 :算法创建了两个名为
dpMax
和dpMin
的整型数组来存储动态规划的所有中间状态。 - 空间大小 :每个数组的长度都与输入数组
nums
的长度N
相同。因此,总的空间占用为 O(N) + O(N) = O(N)。
综合分析 :
算法所需的额外空间主要由 dpMax
和 dpMin
两个数组决定。因此,其空间复杂度为 O(N)。
参考灵神