难度中等715
给你一个整数数组 nums
,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
nums[i]
结尾的连续子数组的最大值;nums[i]
结尾,这是一个经验,可以简化讨论。提示:以
nums[i]
结尾这件事情很重要,贯穿整个解题过程始终,请大家留意。
这里是百度百科的「无后效性」词条的解释:
无后效性是指如果在某个阶段上过程的状态已知,则从此阶段以后过程的发展变化仅与此阶段的状态有关,而与过程在此阶段以前的阶段所经历过的状态无关。利用动态规划方法求解多阶段决策过程问题,过程的状态必须具备无后效性。
再翻译一下就是:「动态规划」通常不关心过程,只关心「阶段结果」,这个「阶段结果」就是我们设计的「状态」。什么算法关心过程呢?「回溯算法」,「回溯算法」需要记录过程,复杂度通常较高。
而将状态定义得更具体,通常来说对于一个问题的解决是满足「无后效性」的。这一点的叙述很理论化,不熟悉朋友可以通过多做相关的问题来理解「无后效性」这个概念。
dp[i][j]
:以nums[i]结尾的连续子数组的最值,计算最大值还是最小值由j 来表示,j 就两个值;
j = 0
的时候,表示计算的是最小值;j = 1
的时候,表示计算的是最大值。这样一来,状态转移方程就容易写出。
nums[i]
必须被选取(请大家体会这一点,这一点恰恰好也是使得子数组、子序列问题更加简单的原因:当情况复杂、分类讨论比较多的时候,需要固定一些量,以简化计算);nums[i]
的正负和之前的状态值(正负)就产生了联系,由此关系写出状态转移方程:
nums[i] = 0
的时候,由于 nums[i]
必须被选取,最大值和最小值都变成 00 ,合并到上面任意一种情况均成立。nums[i] > 0
是,如果 dp[i - 1][1] < 0
(之前的状态最大值) ,此时 nums[i]
可以另起炉灶(这里依然是第 53 题的思想),此时 dp[i][1] = nums[i]
,合起来写就是:dp[i][1] = max(nums[i], nums[i] * dp[i - 1][1]) if nums[i] >= 0
其它三种情况可以类似写出,状态转移方程如下:
dp[i][0] = min(nums[i], nums[i] * dp[i - 1][0]) if nums[i] >= 0
dp[i][1] = max(nums[i], nums[i] * dp[i - 1][1]) if nums[i] >= 0
dp[i][0] = min(nums[i], nums[i] * dp[i - 1][1]) if nums[i] < 0
dp[i][1] = max(nums[i], nums[i] * dp[i - 1][0]) if nums[i] < 0
由于 nums[i]
必须被选取,那么 dp[i][0] = nums[0],dp[i][1] = nums[0]
。
题目问连续子数组的乘积最大值,这些值需要遍历 dp[i][1]
获得。
参考代码 1:
public class Solution {
public int maxProduct(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
// dp[i][0]:以 nums[i] 结尾的连续子数组的最小值
// dp[i][1]:以 nums[i] 结尾的连续子数组的最大值
int[][] dp = new int[len][2];
dp[0][0] = nums[0];
dp[0][1] = nums[0];
for (int i = 1; i < len; i++) {
if (nums[i] >= 0) {
dp[i][0] = Math.min(nums[i], nums[i] * dp[i - 1][0]);
dp[i][1] = Math.max(nums[i], nums[i] * dp[i - 1][1]);
} else {
dp[i][0] = Math.min(nums[i], nums[i] * dp[i - 1][1]);
dp[i][1] = Math.max(nums[i], nums[i] * dp[i - 1][0]);
}
}
// 只关心最大值,需要遍历
int res = dp[0][1];
for (int i = 1; i < len; i++) {
res = Math.max(res, dp[i][1]);
}
return res;
}
}
复杂度分析:
问题做到这个地方,其实就可以了。下面介绍一些非必要但进阶的知识。
参考代码 2:
public class Solution {
public int maxProduct(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
int preMax = nums[0];
int preMin = nums[0];
// 滚动变量
int curMax;
int curMin;
int res = nums[0];
for (int i = 1; i < len; i++) {
if (nums[i] >= 0) {
curMax = Math.max(preMax * nums[i], nums[i]);
curMin = Math.min(preMin * nums[i], nums[i]);
} else {
curMax = Math.max(preMin * nums[i], nums[i]);
curMin = Math.min(preMax * nums[i], nums[i]);
}
res = Math.max(res, curMax);
// 赋值滚动变量
preMax = curMax;
preMin = curMin;
}
return res;
}
}
复杂度分析:
这里说一点题外话:除了基础的「0-1」背包问题和「完全背包」问题,需要掌握「表格复用」的技巧以外。在绝大多数情况下,在「力扣」上做的「动态规划」问题都可以不考虑「表格复用」。
做题通常可以不先考虑优化空间(个人观点,仅供参考),理由如下:
以上个人建议,仅供参考。
动态规划问题通常用于计算多阶段决策问题的最优解。
动态规划有三个概念很重要:
动态规划有两个特别关键的步骤:
动态规划问题思考的两个方向: