[LeetCode] 152. Maximum Product Subarray 求最大子数组乘积

 

Given an integer array nums, find the contiguous subarray within an array (containing at least one number) which has the largest product.html

Example 1:java

Input: [2,3,-2,4]
Output: 
Explanation: [2,3] has the largest product 6.
6

Example 2:git

Input: [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

 

这个求最大子数组乘积问题是由最大子数组之和 Maximum Subarray 演变而来,可是却比求最大子数组之和要复杂,由于在求和的时候,遇到0,不会改变最大值,遇到负数,也只是会减少最大值而已。而在求最大子数组乘积的问题中,遇到0会使整个乘积为0,而遇到负数,则会使最大乘积变成最小乘积,正由于有负数和0的存在,使问题变得复杂了很多。好比,如今有一个数组 [2, 3, -2, 4],能够很容易的找出全部的连续子数组,[2],[3],[-2],[4],[2, 3],[3, -2],[-2, 4],[2, 3, -2],[3, -2, 4],[2, 3, -2, 4],而后能够很轻松的算出最大的子数组乘积为6,来自子数组 [2, 3]。但如何写代码来实现自动找出最大子数组乘积呢,博主最早想到的方比较简单粗暴,就是找出全部的子数组,而后算出每个子数组的乘积,而后比较找出最大的一个,须要两个 for 循环,第一个 for 遍历整个数组,第二个 for 遍历含有当前数字的子数组,就是按如下顺序找出子数组: [2],[2, 3],[2, 3, -2],[2, 3, -2, 4],[3],[3, -2],[3, -2, 4],[-2],[-2, 4],[4],在本地测试的一些数组所有经过,因而兴高采烈的拿到 OJ 上测试,结果丧心病狂的 OJ 用一个有 15000 个数字的数组来测试,而后说程序的运行时间超过了要求值,一看代码,果真如此,时间复杂度 O(n2), 得想办法只用一次循环搞定。想来想去想不出好方法,因而到网上搜各位大神的解决方法。其实这道题最直接的方法就是用 DP 来作,并且要用两个 dp 数组,其中 f[i] 表示子数组 [0, i] 范围内而且必定包含 nums[i] 数字的最大子数组乘积,g[i] 表示子数组 [0, i] 范围内而且必定包含 nums[i] 数字的最小子数组乘积,初始化时 f[0] 和 g[0] 都初始化为 nums[0],其他都初始化为0。那么从数组的第二个数字开始遍历,那么此时的最大值和最小值只会在这三个数字之间产生,即 f[i-1]*nums[i],g[i-1]*nums[i],和 nums[i]。因此用三者中的最大值来更新 f[i],用最小值来更新 g[i],而后用 f[i] 来更新结果 res 便可,因为最终的结果不必定会包括 nums[n-1] 这个数字,因此 f[n-1] 不必定是最终解,不断更新的结果 res 才是,参见代码以下:github

 

解法一:数组

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int res = nums[0], n = nums.size();
        vector<int> f(n, 0), g(n, 0);
        f[0] = nums[0];
        g[0] = nums[0];
        for (int i = 1; i < n; ++i) {
            f[i] = max(max(f[i - 1] * nums[i], g[i - 1] * nums[i]), nums[i]);
            g[i] = min(min(f[i - 1] * nums[i], g[i - 1] * nums[i]), nums[i]);
            res = max(res, f[i]);
        }
        return res;
    }
};

 

咱们能够对上面的解法进行空间上的优化,如下摘自 OJ 官方解答,大致思路相同,写法更加简洁:ide

Besides keeping track of the largest product, we also need to keep track of the smallest product. Why? The smallest product, which is the largest in the negative sense could become the maximum when being multiplied by a negative number.post

Let us denote that:测试

f(k) = Largest product subarray, from index 0 up to k.

 

Similarly,优化

g(k) = Smallest product subarray, from index 0 up to k.

 

Then,url

f(k) = max( f(k-1) * A[k], A[k], g(k-1) * A[k] )
g(k) = min( g(k-1) * A[k], A[k], f(k-1) * A[k] )

 

There we have a dynamic programming formula. Using two arrays of size n, we could deduce the final answer as f(n-1). Since we only need to access its previous elements at each step, two variables are sufficient.

public int maxProduct(int[] A) {
   assert A.length > 0;
   int max = A[0], min = A[0], maxAns = A[0];
   for (int i = 1; i < A.length; i++) {
      int mx = max, mn = min;
      max = Math.max(Math.max(A[i], mx * A[i]), mn * A[i]);
      min = Math.min(Math.min(A[i], mx * A[i]), mn * A[i]);
      maxAns = Math.max(max, maxAns);
   }
   return maxAns;
}

 

根据上述描述能够写出代码以下:

 

解法二:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        if (nums.empty()) return 0;
        int res = nums[0], mn = nums[0], mx = nums[0];
        for (int i = 1; i < nums.size(); ++i) {
            int tmax = mx, tmin = mn;
            mx = max(max(nums[i], tmax * nums[i]), tmin * nums[i]);
            mn = min(min(nums[i], tmax * nums[i]), tmin * nums[i]);
            res = max(res, mx);
        }
        return res;
    }
};

 

下面这种方法也是用两个变量来表示当前最大值和最小值的,可是没有无脑比较三个数,而是对于当前的 nums[i] 值进行了正负状况的讨论:

1. 当遍历到一个正数时,此时的最大值等于以前的最大值乘以这个正数和当前正数中的较大值,此时的最小值等于以前的最小值乘以这个正数和当前正数中的较小值。

2. 当遍历到一个负数时,先用一个变量t保存以前的最大值 mx,而后此时的最大值等于以前最小值乘以这个负数和当前负数中的较大值,此时的最小值等于以前保存的最大值t乘以这个负数和当前负数中的较小值。

3. 在每遍历完一个数时,都要更新最终的最大值。

P.S. 若是这里改为求最小值的话,就是求最小子数组乘积,而且时间复杂度是醉人的 O(n),是否是很强大呢,参见代码以下:

 

解法三:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int res = nums[0], mx = res, mn = res;
        for (int i = 1; i < nums.size(); ++i) {
            if (nums[i] > 0) {
                mx = max(mx * nums[i], nums[i]);
                mn = min(mn * nums[i], nums[i]);
            } else {
                int t = mx;
                mx = max(mn * nums[i], nums[i]);
                mn = min(t * nums[i], nums[i]);
            }
            res = max(res, mx);
        }
        return res;
    }
};

 

下面这道题使用了一个 trick 来将上面解法的分状况讨论合成了一种,在上面的解法中分析了当 nums[i] 为正数时,最大值和最小值的更新状况,为负数时,稍有不一样的就是最小值更新时要用到以前的最大值,而不是更新后的最大值,因此才要用变量t来保存以前的结果。而下面这种方法的巧妙处在于先判断一个当前数字是不是负数,是的话就交换最大值和最小值。那么此时的 mx 就是以前的 mn,因此 mx 的更新仍是跟上面的方法是统一的,而在在更新 mn 的时候,以前的 mx 已经保存到 mn 中了,并且并无改变,因此能够直接拿来用,不得不说,确实叼啊,参见代码以下:

 

解法四:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int res = nums[0], mx = res, mn = res;
        for (int i = 1; i < nums.size(); ++i) {
            if (nums[i] < 0) swap(mx, mn);
            mx = max(nums[i], mx * nums[i]);
            mn = min(nums[i], mn * nums[i]);
            res = max(res, mx);
        }
        return res;
    }
};

 

再来看一种画风不太同样的解法,这种解法遍历了两次,一次是正向遍历,一次是反向遍历,至关于正向创建一个累加积数组,每次用出现的最大值更新结果 res,而后再反响创建一个累加积数组,再用出现的最大值更新结果 res,注意当遇到0的时候,prod 要重置为1。至于为啥正反两次遍历就能够获得正确的结果了呢?主要仍是因为负数个数的关系,由于负数可能会把最大值和最小值翻转,那么当有奇数个负数时,若是只是正向遍历的话,可能会出错,好比 [-1, -2, -3],累加积会获得 -1,2,-6,看起来最大值只能为2,其实不对,而若是咱们再反向来一遍,累加积为 -3,6,-6,就能够获得6了。因此当负数个数为奇数时,首次出现和末尾出现的负数就很重要,有可能会是最大积的组成数字,因此遍历两次就不会漏掉组成最大值的机会,参见代码以下:

 

解法五:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int res = nums[0], prod = 1, n = nums.size();
        for (int i = 0; i < n; ++i) {
            res = max(res, prod *= nums[i]);
            if (nums[i] == 0) prod = 1;
        }
        prod = 1;
        for (int i = n - 1; i >= 0; --i) {
            res = max(res, prod *= nums[i]);
            if (nums[i] == 0) prod = 1;
        }
        return res;
    }
};

 

Github 同步地址:

https://github.com/grandyang/leetcode/issues/152

 

相似题目:

Maximum Subarray

House Robber

Product of Array Except Self

Maximum Product of Three Numbers

Subarray Product Less Than K

 

参考资料:

https://leetcode.com/problems/maximum-product-subarray/

https://leetcode.com/problems/maximum-product-subarray/discuss/48302/2-passes-scan-beats-99

https://leetcode.com/problems/maximum-product-subarray/discuss/48261/share-my-dp-code-that-got-ac

https://leetcode.com/problems/maximum-product-subarray/discuss/48252/sharing-my-solution-o1-space-on-running-time

https://leetcode.com/problems/maximum-product-subarray/discuss/48230/possibly-simplest-solution-with-on-time-complexity

https://leetcode.com/problems/maximum-product-subarray/discuss/48389/my-concise-dp-on-java-solution-with-o1-extra-space

 

LeetCode All in One 题目讲解汇总(持续更新中...)

相关文章
相关标签/搜索