【LeetCode 热题 100】152. 乘积最大子数组——(解法一)递推
Problem: 152. 乘积最大子数组
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N)
- 空间复杂度:O(N)
整体思路
这段代码旨在解决经典的 “乘积最大子数组” (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
数组后,需要遍历它来找到其中的最大值作为最终答案。
完整代码
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)。
参考灵神