1. 程式人生 > >[LeetCode] Maximum Product Subarray 求最大子陣列乘積

[LeetCode] Maximum Product Subarray 求最大子陣列乘積

Find the contiguous subarray within an array (containing at least one number) which has the largest product.

For example, given the array [2,3,-2,4],
the contiguous subarray [2,3] has the largest product = 6.

這個求最大子陣列乘積問題是由最大子陣列之和問題演變而來,但是卻比求最大子陣列之和要複雜,因為在求和的時候,遇到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]範圍內的最大子陣列乘積,g[i]表示子陣列[0, 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即可,參見程式碼如下:

解法一:

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官方解答,大體思路相同,寫法更加簡潔:

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.

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,

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,參見程式碼如下:

解法五:

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;
    }
};

類似題目:

參考資料: