53. 最大子序和(劍指 Offer 42)

Curryxin發表於2021-08-11

53. 最大子序和(劍指 Offer 42)

知識點:陣列;字首和;哨兵;動態規劃;貪心;分治

題目描述

輸入一個整型陣列,陣列中的一個或連續多個整陣列成一個子陣列。求所有子陣列的和的最大值。

要求時間複雜度為O(n)。

示例
輸入: nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子陣列 [4,-1,2,1] 的和最大,為 6。

解法一:字首和+哨兵

連續子陣列 --> 字首和
從前往後遍歷求字首和,維持兩個變數,一個是最大子陣列和,也就是答案,一個是最小的字首和,我們可以把這個值理解為哨兵,這個就是我們用來獲取答案的,因為每次字首和-這個最小的肯定就是最大的。

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0; //字首和;
        int minPre = 0; //最小的字首和:哨兵;
        int maxSum = Integer.MIN_VALUE;
        for(int i = 0; i < nums.length; i++){
            pre += nums[i];
            maxSum = Math.max(maxSum, pre-minPre);
            minPre = Math.min(pre, minPre);
        }
        return maxSum;
    }
}

解法二:貪心

這道題貪心怎麼解?貪什麼呢?想一下在這個過程中,比如-2 1,我們需要-2嗎?不需要!因為負數只會拉低我們最後的和,只起副作用的索性不如不要了。直接從1開始就行了; 貪的就是負數和一定會拉低結果。
所以我們的貪心選擇策略就是:只選擇和>0的,對於和<=0的都可以捨棄了。

class Solution {
    public int maxSubArray(int[] nums) {
        int maxSum = Integer.MIN_VALUE;
        int sum = 0;
        for(int i = 0; i < nums.length; i++){
            sum += nums[i];
            maxSum = Math.max(sum, maxSum);
            if(sum <= 0){
                sum = 0; //對於<=0的字首和,已經沒要意義了,從下一位置開始;
                continue;
            }
        }
        return maxSum;
    }
}

解法三:分治

這道題可以用分治去解。期望去求解一個區間[l,r]內的最大子序和,按照分而治之的思想,可以將其分為左區間和右區間。
左區間L:[l, mid]和右區間R:[mid + 1, r].
lSum 表示 [l,r] 內以 l 為左端點的最大子段和
rSum 表示 [l,r] 內以 r 為右端點的最大子段和
mSum 表示 [l,r] 內的最大子段和
iSum 表示 [l,r] 的區間和
遞迴地求解出L.mSum以及R.mSum之後求解M.mSum。因此首先在分治的遞迴過程中需要維護區間最大連續子列和mSum這個資訊。
接下來分析如何維護M.mSum。具體來說有3種可能:

  • M上的最大連續子列和序列完全在L中,即M.mSum = L.mSum
  • M上的最大連續子列和序列完全在R中,即M.mSum = R.mSum
  • M上的最大連續子列和序列橫跨L和R,則該序列一定是從L中的某一位置開始延續到mid(L的右邊界),然後從mid + 1(R的左邊界)開始延續到R中的某一位置。因此我們還需要維護區間左邊界開始的最大連續子列和leftSum以及區間右邊界結束的最大連續子列和rightSum資訊
class Solution {
    public class Status{
        public int lSum, rSum, mSum, iSum;
        // lSum 表示 [l,r] 內以 l 為左端點的最大子段和
        // rSum 表示 [l,r] 內以 r 為右端點的最大子段和
        // mSum 表示 [l,r] 內的最大子段和
        // iSum 表示 [l,r] 的區間和
        public Status(int lSum, int rSum, int mSum, int iSum){
            this.lSum = lSum;
            this.rSum = rSum;
            this.mSum = mSum;
            this.iSum = iSum;
        }
    }
    public Status getInfo(int[] a, int l, int r){
        if(l == r) return new Status(a[l], a[l], a[l], a[l]); //終止條件;
        int mid = l + ((r-l) >> 1);
        Status lsub = getInfo(a, l, mid);
        Status rsub = getInfo(a, mid+1, r);
        return pushUp(lsub, rsub); 
    }
    //根據兩個子串得到整個序列結果;
    public Status pushUp(Status l, Status r){
        int iSum = l.iSum + r.iSum;
        int lSum = Math.max(l.lSum, l.iSum+r.lSum);
        int rSum = Math.max(r.rSum, r.iSum+l.rSum);
        int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum+r.lSum);
        return new Status(lSum, rSum, mSum, iSum);
    }

    public int maxSubArray(int[] nums) {
        return getInfo(nums, 0, nums.length-1).mSum;
    }
}

解法四:動態規劃

  • 1.確定dp陣列和其下標的含義:dp[i]表示以i結尾的連續子陣列的最大和;
  • 2.確定遞推公式,即狀態轉移方程:以i結尾想一下我們有幾種可能,一種是i-1過來的,也就是上一個的連續子陣列延續到i處了,那和就為dp[i-1]+nums[i],另一種呢,就是自己開始,前面那個連續子陣列不行,那就是nums[i]了,想一下為什麼前面那個不行,還不是前面的和會拖累自己,那就意味著前面的和是負數;這其實就引出貪心的方法了。不過我們這裡不用這麼麻煩,直接用一個max函式,取兩者大的那個就行;
  • 3.dp初始化base case:dp[0]只有一個數,所以dp[0] = nums[0];
class Solution {
    public int maxSubArray(int[] nums) {
        int len = nums.length;
        int[] dp = new int[len]; //以i結尾的連續子陣列的最大和為dp[i];
        if(nums == null || len <= 1) return nums[0];
        dp[0] = nums[0];
        for(int i = 1; i < len; i++){
            dp[i] = Math.max(dp[i-1]+nums[i], nums[i]); //狀態轉移;
        }
        //注意我們要遍歷一遍返回最大的dp;
        int maxSum = dp[0];
        for(int i = 1; i < len; i++){
            maxSum = Math.max(maxSum, dp[i]);
        }
        return maxSum;
    }
}

當然上述程式可以優化,因為我們的dp[i]其實只和前一狀態i-1有關,所以可以採用一個滾動變數來記錄,而不用整個陣列。

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0;  //記錄前一狀態;
        int res = nums[0]; //記錄最後結果的最大值;
        for (int num : nums) {
            pre = Math.max(pre + num, num);
            res = Math.max(res, pre);
        }
        return res;
    }
}

體會

這道題目是一道很典型的題目,用到了各種方法和思想。要常看常做,分治是其中比較困難的,但是要會這種思想。這道題目最好的方法還是哨兵和動態規劃, 其實貪心就是從動態規劃的一個特殊情況過去的,體會兩者的關係;

相關文章