動態規劃---例題3.最大子段和問題

PGokc發表於2021-11-27

本題與力扣主站53題 --- 最大子序和相同.

一.問題描述

給定n個整數(可能有負數)組成的序列a1,a2,…an, 求子段和ai+ai+1+…+aj的最大值。
當所有整數均小於零時,定義其子段和為0。
最大值為max{0, maxΣak}
例:(-2, 11, -4, 13, -5, -2)的最大子段和為20

二.解題思路

1.樸素暴力
我們使用陣列a存放n個整數,sum、besti、bestj分別存放最大子段和及其始末下標。
時間複雜度: T(n) = O(n^3)

int MaxSum(int n, int *a, int &besti, int &bestj)
{
    int sum = 0;
    for(int i=1; i<=n; i++)
        for(int j=i; j<=n; j++)  //i,j邊界確定下來
        {
            int thissum = 0;
            for(int k=i; k<=j; k++) thissum += a[k];  //(用k來遍歷a[i,j]求和)每次k只是加一,所以重複計算了a[i]~a[j]的和
            if(thissum > sum)
            {
                sum = thissum;
                besti = i;
                bestj = j;
            }
        }
    return sum;
}

對上述方法的改進:
我們用k來遍歷a[i...j]求和,每次k只是加一,所以重複計算了a[i]~a[j]的和.故我們可以省略一個迴圈,每次j++後,只要把最後一個a[k] (此時k=j)加上就好.
時間複雜度: T(n) = O(n^2)

int MaxSum2(int n, int *a, int &besti, int &bestj)
{
    int sum = 0;
    for(int i=1; i<=n; i++)
    {
        int thissum = 0;
        for(int j=i; j<=n; j++)
        {
            thissum += a[j];
            if(thissum > sum)
            {
                sum = thissum;
                besti = i;
                bestj = j;
            }
        }
    }
    return sum;
}

2.分治演算法

將a[1:n]分為a[1:n/2]和a[n/2+1:n]兩部分,一共分三種情形:
image-20211028164334885

  • 最大子段和在左半部分,比如LS
  • 最大子段和在右半部分,比如RS
  • 最大子段和跨越兩部分,為S1+S2,並且左半部分的最右端元素一定在S1中,右半部分的最左端元素一定在S2中。

最大子段和等於: Max(LS, RS, S1+S2)
時間複雜度:T(n) = O(n log n)

int MaxSubSum13(int *a, int left, int right)
{
    int sum = 0;
    if(left == right) sum = a[left]>0 ? a[left]:0;
    else
    {
        int center = (left+right)/2;
        int leftsum = MaxSubSum13(a, left, center);
        int rightsum = MaxSubSum13(a, center+1, right);
        
        int s1 = 0;
        int lefts = 0;
        for(int i=center; i>=left; i--)  //從center位置往左和往右分別找到最大值
        {
            left += a[i];
            if(lefts > s1) s1 = lefts;
        }
        int s2 = 0;
        int rights = 0;
        for(int i=center+1; i<=right; i++)
        {
            right += a[i];
            if(rights > s2) s2 = rights;
        }
        sum = s1+s2;
        if(sum<leftsum) sum = leftsum;
        if(sum<rightsum) sum = rightsum;
    }
    return sum;
}
int MaxSum13(int n, int *a)
{
    return MaxSubSum13(a, 1, n);
}

計算時間:遞迴方程

  • T(n)=O(1) n<=C

  • T(n)=2T(n/2)+O(n) n > C

    故T(n)=O(n logn)

3.動態規劃(重點)

假設nums陣列長度為n, 我們用 f(i) 代表以第 i 個數結尾的「連續子陣列的最大和」,
那麼很顯然我們要求的答案就是:max(0<i<n){f(i)}

因此我們只需要求出每個位置的 f(i),然後返回 f 陣列中的最大值即可。那麼我們如何求 f(i) 呢?
我們可以考慮 nums[i], 單獨成為一段還是加入f(i-1) 對應的那一段.這取決於 nums[i] 和 f(i-1) + nums[i] 的大小,我們希望獲得一個比較大的,於是可以寫出這樣的動態規劃轉移方程:

  • f(i) = max(nums[i], f(i-1) + nums[i]) 1<=i<=n

時間複雜度: T(n) = 0(n)

int maxSubArray3(vector<int>& nums)
{
    if(nums.size()==0) return {};
    int n = nums.size();
    int pre = 0;
    int maxSum = nums[0];
    for(int i=0; i<n; i++)   //for(const auto   & x : nums) 
    {
        pre = max(pre+nums[i], nums[i]);
        maxSum = max(pre, maxSum);
    }
    return maxSum;
}
本篇文章參考我的老師畢方明《演算法設計與分析》課件.

歡迎大家訪問我的個人部落格 --- 喬治的程式設計小屋,和我一起為大廠offer 努力吧!

相關文章