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

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

知識點:數組;前綴和;哨兵;動態規劃;貪心;分治數組

題目描述

輸入一個整型數組,數組中的一個或連續多個整數組成一個子數組。求全部子數組的和的最大值。函數

要求時間複雜度爲O(n)。優化

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

解法一:前綴和+哨兵

連續子數組 --> 前綴和
從前日後遍歷求前綴和,維持兩個變量,一個是最大子數組和,也就是答案,一個是最小的前綴和,咱們能夠把這個值理解爲哨兵,這個就是咱們用來獲取答案的,由於每次前綴和-這個最小的確定就是最大的。this

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的均可以捨棄了。code

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有關,因此能夠採用一個滾動變量來記錄,而不用整個數組。get

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

體會

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

相關文章
相關標籤/搜索