「前端刷题」152.乘积最大子数组(MEDIUM)

90 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

题目(Maximum Product Subarray)

链接:https://leetcode-cn.com/problems/maximum-product-subarray
解决数:1705
通过率:42.6%
标签:数组 动态规划 
相关公司:amazon linkedin google 

给你一个整数数组 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] <= 10
  • nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数

思路

  • 最开始看到这个题目的第一反应想到的动态转移方程是,当前元素是不是能跟前面一个元素构成更大子数组,如果能跟前面一个元素构成更大乘积,那就加入前面行列中,如果跟前面一个元素构成的乘积反而更小了,那还不如自立门户呢;总之就是哪个大,当前的明智选择就选哪个
  • 所以一开始我的思路是dp[i]的定义是以 i 项为末尾项的子数组们中的最大乘积
  • 动态转移方程是
    dp[i] = Math.max(
      // 乘上之前的积更小了还不如自立山头
      nums[i],
      // 当前元素乘之前子数组的乘积
      dp[i - 1] * nums[i]
    );
  • 所以写了如下的代码:
/**
 * @param {string[]} nums
 * @return {number}
 */
var maxProduct = function (nums) {
  let n = nums.length;
  let dp = new Array(n).fill(0);
  // base case
  dp[0] = nums[0];
  for (let i = 1; i < n; i++) {
    dp[i] = Math.max(
      // 乘上之前的积更小了还不如自立山头
      nums[i],
      // 当前元素乘之前子数组的乘积
      dp[i - 1] * nums[i]
    );
  }
  return Math.max(...dp);
};
  • 一运行,通不过,因为没有考虑负负得正的情况,比方说如果nums = [-2, 3, -4];
  • 所以深入一步想,对于dp[i]我们不仅仅要保存乘积的最大值,也需要保存乘积的最小值
  • 以上面的例子来说,dp[1]里面不仅仅需要保存最小乘积-6,也需要保存最大乘积3,这样当后面一位数字是负数,最小乘积就能派上用场了,比方说-4就应乘-6得到最大乘积24,同理如果后面一位是正数,就应该用最大乘积
  • 所以,dp[i] 要放两项
  1. dp[i][0]: 从第 0 项到第 i 项范围内的子数组的最小乘积
  2. dp[i][1]: 从第 0 项到第 i 项范围内的子数组的最大乘积
  • 状态转移方程:
  1. 自立门户;不和别人乘,就 nums[i] 自己
  2. nums[i]是负数,希望乘上前面的最小乘积
  3. nums[i]是正数,希望乘上前面的最大乘积
  • 题目结果在dp的第1维里面找最大值

代码

/**
 * @param {string[]} nums
 * @return {number}
 */
var maxProduct = function (nums) {
  let n = nums.length;
  let dp = new Array(n).fill(0).map(() => new Array(2).fill(0));
  /* base case */
  // 从第 0 项到第 i 项范围内的子数组的最小乘积
  dp[0][0] = nums[0];
  // 从第 0 项到第 i 项范围内的子数组的最大乘积
  dp[0][1] = nums[0];
  for (let i = 1; i < n; i++) {
    dp[i][0] = Math.min(
      // 不和别人乘就nums[i]自己
      nums[i],
      // nums[i]是负数,希望乘上前面的最小乘积
      dp[i - 1][0] * nums[i],
      // nums[i]是正数,希望乘上前面的最大乘积
      dp[i - 1][1] * nums[i]
    );
    dp[i][1] = Math.max(
      // 不和别人乘就nums[i]自己
      nums[i],
      // nums[i]是负数,希望乘上前面的最小乘积
      dp[i - 1][0] * nums[i],
      // nums[i]是正数,希望乘上前面的最大乘积
      dp[i - 1][1] * nums[i]
    );
  }
  return Math.max(...dp.map((item) => item[1]));
};

动态规划状态压缩版本的解题思路

  • 从上面来看,我们定义了一个dp数组,深入一想,其实每次dp[i]只依赖于dp[i-1],与之前的dp[i-n]没有任何关系
  • 所以我们用两个变量来保存上一次乘积的最大值和上一次乘积的最小值,在循环迭代的时候根据上一次乘积的最大值和最小值来推导出当前这一次的最大值和最小值
/**
 * @param {string[]} nums
 * @return {number}
 */
var maxProduct = function (nums) {
  let n = nums.length;
  // 上一个最小的乘积
  let prevMin = nums[0],
    // 上一个最大的乘积
    prevMax = nums[0];
  /* base case */
  let res = nums[0];
  for (let i = 1; i < n; i++) {
    let tempMin = prevMin,
      tempMax = prevMax;
    prevMin = Math.min(
      // 不和别人乘就nums[i]自己
      nums[i],
      // nums[i]是负数,希望乘上前面的最小乘积
      tempMin * nums[i],
      // nums[i]是正数,希望乘上前面的最大乘积
      tempMax * nums[i]
    );
    prevMax = Math.max(
      // 不和别人乘就nums[i]自己
      nums[i],
      // nums[i]是负数,希望乘上前面的最小乘积
      tempMin * nums[i],
      // nums[i]是正数,希望乘上前面的最大乘积
      tempMax * nums[i]
    );
    res = Math.max(res, prevMax);
  }
  return res;
};