"子序列、子数组、子串,傻傻分不清楚"
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
152. 乘积最大子数组 题目描述:给你一个整数数组 ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。(NOTE: 测试用例的答案是一个 32-位 整数。a子数组 是数组的连续子序列)
| 示例1 | 示例2 |
|---|---|
| 输入: nums = 输出: 解释: 子数组 有最大乘积 。 | 输入: nums = 输出: 解释: 结果不能为 , 因为 不是子数组。 |
本题与 # 53. 最大子数组和 有相似的地方,但是隐藏着巨大的坑,一不小心就着了面试官的道。
暴力输出
暴力算法很好理解,就是把所有子数组枚举出来,然后分别计算子数组内元素元素的乘积,并选择出最大值。为避免无意义的重复计算,我们定义,
当计算 时,不再从 一直累乘到 ,而是选择 ,从而减低暴力算法的时间复杂度。
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n^2),n是nums数组的长度
*/
function maxProduct(nums: number[]): number {
const n = nums.length;
let ans = Number.MIN_SAFE_INTEGER;
for(let i = 0; i < n; i++) {
let product = 1;
for(let j = i; j < n; j++) {
product = product * nums[j];
ans = Math.max(ans, product);
}
}
return ans
}
中规中矩的动态规划
设问:我们是否可以参考 # 53. 最大子数组和 的求解思想,定义 是以 结尾的子数组元素最大乘积,且同样规划「最优子结构」为
❌ 答案是否定的。至关重要的一点就是 非正整数 对子数组元素乘积的影响,非正整数的存在可以使上一个状态变成最大值或者最小值,因此当前位置的最优解(状态)未必是由前一个位置的最优解转移得到的。简单证明一下:
假设我们只需要一维状态 。对于 的符号,一定有三种情况,分别是
同理,对于 的符号,也一定有三种情况,分别是
🤔 按照 # 53. 最大子数组和 解题的递推方式来看,当 且 时 。 💥 NOTE: 此时我们发现 这个状态完全舍弃。如果在区间 ,仅存在一个元素 ,那么我们丢失了一个使子数组元素乘积变为更大的“潜在因子”( 可让最后的结果瞬间反转,最小值突然变成最大值),即 。
因此我们可以多设置一种状态 来保存 结尾子数组元素的最小乘积,防止丢失非正整数可能来子数组元素最大乘积带来的“正向”影响。
1、确定 dp 状态数组
设 是以 结尾的子数组元素最大乘积
设 是以 结尾的子数组元素最小乘积
其中
2、确定 dp 状态转移方程
为保证连续性, 和 必须得有 的参与。
为保证最大性,如果 是负数,那我们希望 也同时为负数,让乘积负负为正,扭转“劣势”;如果 是正数,那我们希望 也同时为正数,让乘积更大。故,我们得出如下状态转移方程:
3、确定 dp 初始状态
4、确定遍历顺序
从 遍历到
5、确实最终返回值
重温初心: 是以 结尾的最大的子数组乘积,故 仅仅是以 元素结尾的最大子数组乘积,并不代表全局的最大,故需要全局对比。所以,返回值为 。
NOTE: 代表将数组中所有元素按照其索引按顺序传到 函数。
6、代码示例
/**
* 空间复杂度 O(n),n是nums数组的长度
* 时间复杂度 O(n)
*/
function maxProduct(nums: number[]): number {
const n = nums.length;
const dp_max = Array.from({ length: n }, () => 0);
const dp_min = Array.from({ length: n }, () => 0);
dp_max[0] = nums[0];
dp_min[0] = nums[0];
for(let i = 1; i < n; i++){
dp_max[i] = Math.max(dp_max[i - 1] * nums[i], dp_min[i - 1] * nums[i], nums[i]);
dp_min[i] = Math.min(dp_max[i - 1] * nums[i], dp_min[i - 1] * nums[i], nums[i]);
}
return Math.max(...dp_max);
}
空间压缩
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n),n是nums数组的长度
*/
function maxProduct(nums: number[]): number {
const n = nums.length;
let dpMax = nums[0];
let dpMin = nums[0];
let ans = nums[0];
for(let i = 1; i < n; i++){
const tempMax = dpMax * nums[i];
const tempMin = dpMin * nums[i];
dpMax = Math.max(tempMax, tempMin, nums[i]);
dpMin = Math.min(tempMax, tempMin, nums[i]);
ans = Math.max(ans, dpMax);
}
return ans;
}
思维提升
我们把数组元素分成两类,1)值为非 的元素,2)值为 的元素。
-
在第一种情况下,如果一个子列中负数有偶数个,那所有数乘起来就是最大值;如果负数有奇数个,那从最左一直乘到最后一个负数最大 或者从右一直乘到最前一个负数最大。
-
在第二种情况下,出现 元素就保持这个元素(因为这个元素是唯一能确定值的大小),下一次循环迭代时重新计算。
因此这个问题就是被0划分的每个小数组中做负数奇偶个数的讨论。此处采用正向、反向两次“前缀积”的方式,找到子数组元素的最大乘积。
/**
* 空间复杂度 O(n),n是nums数组的长度
* 时间复杂度 O(n)
*/
function maxProduct(nums: number[]): number {
const n = nums.length;
const inOrder = [...nums];
const reOrder = [...nums.reverse()];
for(let i = 1; i < n; i++) {
inOrder[i] *= (inOrder[i - 1] === 0 ? 1 : inOrder[i - 1]);
reOrder[i] *= (reOrder[i - 1] === 0 ? 1 : reOrder[i - 1]);
}
return Math.max(...inOrder, ...reOrder);
}