53. Maximum Subarray (Kadane算法 / 動態規劃 / 分治法)

Maximum Subarray

【題目】

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

Example:

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Follow up:

If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.

【分析】

這是一道非常簡單的算法題,但是實現的方法卻有很多種。在本篇文章中,博主想介紹三種巧妙的方法,這三種方法在面試和刷題過程中有非常廣泛的應用。

方法一:Kadane算法

算法描述:

  1. 遍歷該數組, 在遍歷過程中, 將遍歷到的元素依次累加起來, 當累加結果小於或等於0時, 從下一個元素開始,重新開始累加。
  2. 累加過程中, 要用一個變量(max_so_far)記錄所獲得過的最大值。
  3. 一次遍歷之後, 變量 max_so_far 中存儲的即爲最大子片段的和值。

Java實現代碼如下:

public class MaximumSubarray {
    public int maxSubArray(int[] nums) {
        int maxEndHere = 0;
        int maxSoFar = 0;
        for (int i = 0; i < nums.length; i++) {
            maxEndHere += nums[i];
            if (maxEndHere < 0) {
                maxEndHere = 0;
            }
            if (maxSoFar < maxEndHere) {
                maxSoFar = maxEndHere;
            }
        }
        return maxSoFar;
    }

理解此算法的關鍵在於:

  1. 最大子片段中不可能包含求和值爲負的前綴。 例如 【-2, 1,4】 必然不能是最大子數列, 因爲去掉值爲負的前綴後【-2,1】, 可以得到一個更大的子數列 【4】、
  2. 所以在遍歷過程中,每當累加結果成爲一個非正值時, 就應當將下一個元素作爲潛在最大子數列的起始元素, 重新開始累加。
  3. 由於在累加過程中, 出現過的最大值都會被記錄, 且每一個可能成爲 最大子數列起始元素 的位置, 都會導致新一輪的累加, 這樣就保證了答案搜索過程的完備性和正確性。

     


方法二:DP(動態規劃)

基本思路是這樣的,在每一步,我們維護兩個變量,一個是全局最優,就是到當前元素爲止最優的解是,一個是局部最優,就是必須包含當前元素的最優的解。接下來說說動態規劃的遞推式(這是動態規劃最重要的步驟,遞歸式出來了,基本上代碼框架也就出來了)。假設我們已知第i步的global[i](全局最優)和local[i](局部最優),那麼第i+1步的表達式是:

  1. local[i+1]=Math.max(A[i], local[i]+A[i]),就是局部最優是一定要包含當前元素,所以不然就是上一步的局部最優local[i]+當前元素A[i](因爲local[i]一定包含第i個元素,所以不違反條件),但是如果local[i]是負的,那麼加上他就不如不需要的,所以不然就是直接用A[i];
  2. global[i+1]=Math(local[i+1],global[i]),有了當前一步的局部最優,那麼全局最優就是當前的局部最優或者還是原來的全局最優(所有情況都會被涵蓋進來,因爲最優的解如果不包含當前元素,那麼前面會被維護在全局最優裏面,如果包含當前元素,那麼就是這個局部最優)。

Java代碼實現如下:

class Solution {
    //Dp
    public int maxSubArray(int[] nums) {
        int maxLocal = nums[0];
        int global = nums[0];
        for (int i = 1; i < nums.length; ++i) {
            maxLocal = Math.max(nums[i], nums[i] + maxLocal);
            global = Math.max(global, maxLocal);
        }
        return global;
    }
}

方法三:分治法

將數組均分爲兩個部分,那麼最大子數組會存在於:

  • 左側數組的最大子數組
  • 右側數組的最大子數組
  • 左側數組的以右側邊界爲邊界的最大子數組+右側數組的以左側邊界爲邊界的最大子數組

假設數組下標有效範圍是l到r,將數組分爲左半部分下標爲(l,mid-1)和右半部分下標爲(mid+1,r)以及中間元素下標爲mid,接下來遞歸求出左半部分的最大子序和:left=helper(nums,l,mid-1); 右半部分最大子序和right=helper(nums,mid+1,r);

接下來再將左半部分右邊界,右半部分左邊界以及中間元素nums[mid]整合,用了兩個循環,先整合左半部分右邊界和中間值,再將整合結果與右半部分左邊界整合得到整合以後的最大子序和max_num,最後返回max_num,left,right的最大值即是要求的最大子序和。

代碼實現如下:


class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.size()==0)return 0;
        return helper(nums,0,nums.size()-1);
    }
    int helper(vector<int>& nums,int l,int r){
        if(l>r)return INT_MIN;//注意此處不是返回0,比如{-2,-1},分治以後變爲左中右n{},-1,{-2}三部分。左半部分{}應返回INT_MIN,
        //因爲還要和右半部分的返回值進行比較,最終正確結果返回-1。若左半部分返回0,0>-2,且大於左中右的最大組合值(-1),最終結果返回0,出錯
        if(l==r)return nums[l];
        int mid=(l+r)/2;
        int left=helper(nums,l,mid-1);
        int right=helper(nums,mid+1,r);
        int t=nums[mid];
        int max_num=nums[mid];
        for(int i=mid-1;i>=l;i--){
            t+=nums[i];
            max_num=max(max_num,t);
        }
        t=max_num;
        for(int i=mid+1;i<=r;i++){
            t+=nums[i];
            max_num=max(max_num,t);
        }
        return max(max(left,right),max_num);
    }
};

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章