1. 程式人生 > >分治法/動態規劃-最大子序和

分治法/動態規劃-最大子序和

給定一個整數陣列 nums ,找到一個具有最大和的連續子陣列(子陣列最少包含一個元素),返回其最大和。

示例:

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子陣列 [4,-1,2,1] 的和最大,為 6。
分治法【時間複雜度O(NlogN)】

概括來說,就是把陣列分成兩個序列,最大和子序列要麼在左半部分,要麼在右半部分,要麼是橫跨左右部分,也就是說包含左半部分的最後一個元素和右半部分的第一個元素。在求左半部分或者右半部分時又再次將序列拆分,如此迴圈。

 * 拆分序列(直到只剩下一個數的序列) ---->   左序列|右序列
 *
求左序列最大值 * 求右序列最大值 * 求跨邊界的最大值 * 求以上三個最大值的最大值 * 不斷重複

舉個栗子:

 * 假定存在以下序列
 * {4 -3 5 -2}
 * 
 * 做拆分
 * {4 -3}{5 -2}
 * 做拆分
 * {4}{-3}
 * 做拆分
 * {4}  求得當前序列最大和為【4】
 * 處理右邊的{-3}
 * {-3}小於0,所以返回0,則最大和為【0】
 * 還有一種就是橫跨左右序列的-3+4=【1】
 * 它們被拆分之前是{4 -3},所以{4 -3}的最大和是max{4,0,1}=【4】
 * 處理右邊的
 * {5 -2}
 * 做拆分
 * {5}
{-2} * 做拆分 * {5} 求得序列最大和為【5】 * 處理右邊的{-2} * {-2}小於0,所以返回0,最大和為【0】 * 還有一種就是橫跨左右序列的-2+5=【3】 * 它們被拆分前是{5 -2},所以{5 -2}最大和是max{5,0,3}=【5】 * {4 -3}{5 -2}處理完畢,最大和分別是【4】【5】 * ------------------------------------------------------------ * 它們被拆分之前是{4 -3 | 5 -2} * 先說結果: * 左邊的最大和(4 + (-3)) = 1 * 右邊的最大和5
* 左右相加得【6】 * * 為什麼是左邊4+(-3)呢? * 首先以|為邊界向左掃描求最大和 * 因為是從【中間向左】掃,第一個掃到的數是-3 * 再向左掃,是44比-3絕對值大吧! * 要包含4,就必須把-3加上 * 因為要跨越邊界,-3肯定要經過的,才能形成連續的序列。 * 左邊掃描完了,就從【中間向右】掃 * 第一個發現5,掃向下一個,發現是-2,相加起來還不如一個5呢 * 於是就不要-2了。為什麼可以不要?因為這是一個從左往右的序列~ * 而-2在最右邊,當然是可以拋棄的了。 * * 如果-2 後面還有一個數,比如3的話,是{5 -2 3},這時把三個數加起來是6,比5大,就需要把三個數都包含起來。 * 因為三個數加起來比5大恩。就是這個理。 * * 此時可以知道{4 -3 | 5 -2},最大和是左邊的1加上右邊的5得到max{4,6,5}=【6】。

採用c++程式碼實現

//求三個數中的最大值
int max3(int a,int b,int c)
{
    return a>b?(a>c?a:c):(b>c?b:c);
}

int maxsubarr(vector<int> &nums,int left,int right){
    //定義 左右端最大值
    int maxleft=0,maxright=0;
    //定義左右端邊界值
    int leftborder=0,rightborder=0;
    //定義左右端邊界最大值
    int maxleftborder=0,maxrightborder=0;
    //遞迴出口,只剩一個數時
    if(left==right)
        return nums[left]>0?nums[left]:0;

    int mid=(left+right)/2;
    //左邊遞迴
    maxleft = maxsubarr(nums,left,mid);
    //右邊遞迴
    maxright = maxsubarr(nums, mid+1, right);
    //求左邊屆最大值
    for(int i=mid;i>=left;i--){
        leftborder+=nums[i];
        if(leftborder>maxleftborder)
            maxleftborder=leftborder;
    }
    //求右邊屆最大值
    for(int i=mid+1;i<=right;i++){
        rightborder+=nums[i];
        if(rightborder>maxrightborder)
            maxrightborder=rightborder;
    }
    return max3(maxleft,maxright,maxleftborder+maxrightborder);
}

int maxSubArr(vector<int>nums){
    return maxsubarr(nums, 0, (int)nums.size()-1);
}
動態規劃【時間複雜度O(N)】

步驟 1:令狀態 dp[i] 表示以 A[i] 作為末尾的連續序列的最大和(這裡是說 A[i] 必須作為連續序列的末尾)。

步驟 2:做如下考慮:因為 dp[i] 要求是必須以 A[i] 結尾的連續序列,那麼只有兩種情況:

  • 這個最大和的連續序列只有一個元素,即以 A[i] 開始,以 A[i] 結尾。
  • 這個最大和的連續序列有多個元素,即從前面某處 A[p] 開始 ,一直到 A[i] 結尾
  • 對第一種情況,最大和就是 A[i] 本身,對第二種情況,最大和是 dp[i-1]+A[i]。

於是得到狀態轉移方程:

dp[i]=maxA[i],dp[i1]+A[i]
這個式子只和 i 與 i 之前的元素有關,且邊界為 dp[0] = A[0],由此從小到大列舉 i,即可得到整個 dp 陣列。接著輸出 dp[0],dp[1],…,dp[n-1] 中的最大子即為最大連續子序列的和。

採用c++程式碼實現

int maxSubArray(vector<int>& nums) {
    int maxsum=0;
    int *dp =new int[nums.size()];
    dp[0]=nums[0];
    maxsum=dp[0];
    for(int i=1;i<nums.size();i++){
        dp[i]=nums[i]+(dp[i-1]>0?dp[i-1]:0);
        maxsum=maxsum>dp[i]?maxsum:dp[i];
    }
    return maxsum;
}