leetcode_53 最大子数组和

1. 题意

给定一个数组,让你求最大子数组和;
所谓的子数组,就是数组中连续的一段。

2. 题解

非常经典的一道题目,值得反复把玩啊!!!

2.1 暴力枚举

首先我们想想怎么枚举子数组。

我们可以固定子数组的左端点,再依次处理子数组的右端点。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {

        int n = nums.size();

        int sum = 0;

        int ans = nums[0];
        for (int i = 0; i < n; i++) {
            sum = 0;
            for (int j = i; j < n; ++j) {
                sum += nums[j];
                ans = max( sum, ans);
            }
        }

        return ans;        
    }
};

时间复杂度 O ( n 2 ) O(n^2) O(n2)

2.2 动态规划

我们可以定义 d p [ n ] dp[n] dp[n]为前 n n n个数组的最大子数组和。

那么状态转移方程为

d p [ n ] = { d p [ n − 1 ] + n u m s [ n − 1 ] , d p [ n − 1 ] > 0 n u m s [ n − 1 ] , d p [ n − 1 ] ≤ 0 \begin{equation*} dp[n] =\begin{cases} dp[n-1] +nums[n-1] , \quad dp[n-1] >0\\ nums[n-1], \quad dp[n-1] \le 0 \end{cases} \end{equation*} dp[n]={dp[n1]+nums[n1],dp[n1]>0nums[n1],dp[n1]0
最终的最大子数组和为
max ⁡ { d p [ i ] } , 1 ≤ i ≤ a r r . s i z e \max \{ dp[i]\} , 1 \le i \le arr.size max{dp[i]},1iarr.size

class Solution {
public:
    int maxSubArray(vector<int>& nums) {

        int n = nums.size();

        vector<int> dp(n + 1, 0);

        dp[1] = nums[0];
        int ans = nums[0];

        for (int i = 1;i < n; ++i) {
            dp[i + 1] = dp[i] > 0 ? dp[i] + nums[i] : nums[i];
            ans = max(ans, dp[i + 1]);
        }
        return ans;        
    }
};

时间复杂度 O ( n ) O(n) O(n), 空间复杂度 O ( n ) O(n) O(n)

我们可以进行空间压缩, 用一个变量代替数组。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {

        int n = nums.size();

        

        int premax = nums[0];
        int ans    = nums[0];

        for (int i = 1;i < n; ++i) {
            premax = premax > 0 ? premax + nums[i] : nums[i];
            ans = max(premax, ans);
        }
    

        return ans;        
    }
};

这样空间复杂度就优化到了 O ( 1 ) O(1) O(1)

2.3 贪心

代码和动态规划的代码几乎一样,但是背后的思想却是不一样的。

就像评论区说的渣男渣女一样,之前的数组和如果小于0了;

那么在下一次元素求和中,我会将你果断的抛弃,我将重新进行选择。

因为你总不能阻止我奔向更好的人吧!

class Solution {
public:

    int maxSubArray(vector<int>& nums) {
        int ans = nums[0];
        int presum = nums[0];
        int n = nums.size();

        for (int i = 1; i < n; ++i) {
            if (presum < 0)
                presum = 0;
            presum += nums[i];
            ans = max(presum, ans);
        }

        return ans;
    }
};
2.4 分治

分治的思路跟归并排序、线段树的思想差不多。

首先分治的核心是把大的问题分成两个规模差不多一样大的子问题,

所以我们先要看怎么将大数组的最大子数组和给转换为小数组的

最大子数组和。

我们假设,数组范围为 [ l , r ] [l,r] [l,r],分为两个子区间 [ l , m i d ] , [ m i d + 1 , r ] [l,mid],[mid+1,r] [l,mid],[mid+1,r]

再令 m s u m ( l , r ) msum(l,r) msum(l,r)表示: [ l , r ] [l,r] [l,r]数组的最大子数组和。

m s u m ( l , r ) = max ⁡ { ∑ i = l ′ r ′ n u m s [ i ]   ∣   l ≤ l ′ , r ′ ≤ r } msum(l,r) = \max \left\{ \sum_{i=l'}^{r'}nums[i]\ \Bigg|\ l \le l',r' \le r \right\} msum(l,r)=max i=lrnums[i]   ll,rr

我们假设最大子数组最终的区间为 [ l ′ , r ′ ] [l',r'] [l,r].

我们很容易的想到 m s u m ( l , m i d )   m s u m ( m i d + 1 , r ) msum(l,mid)\ msum(mid+1,r) msum(l,mid) msum(mid+1,r)

可能是 m s u m ( l , r ) msum(l,r) msum(l,r)的候选值。用更通俗的话来讲,子数组的最大子数组

和可能是数组的最大子数组和。

分别对应了 [ l ′ , r ′ ] ⊆ [ l , m i d ] [l',r'] \subseteq [l,mid] [l,r][l,mid] [ l ′ , r ′ ] ⊆ [ m i d + 1 , r ] [l',r'] \subseteq [mid+1,r] [l,r][mid+1,r]

除此之外,我们还有一种情况,那就是

最大子数组的区间跨越了两个区间,也就是 l ′ ≤ m i d ∩ r ′ ≥ m i d + 1 l'\le mid \cap r' \ge mid+1 lmidrmid+1

此时我们需要分到两个子区间进行处理,也就是 [ l ′ , m i d ]   [ m i d + 1 , r ′ ] [l',mid]\ [mid+1,r'] [l,mid] [mid+1,r]

我们需要找到左下标 l ′ ∈ [ l , m i d ] l'\in[l,mid] l[l,mid]使得, ∑ i = l ′ m i d n u m s [ i ] \sum_{i=l'}^{mid} nums[i] i=lmidnums[i]最大;

同理我们需要找到右下标 r ′ ∈ [ m i d + 1 , r ] r'\in[mid+1,r] r[mid+1,r]使得, ∑ i = m i d + 1 r ′ n u m s [ i ] \sum_{i=mid+1}^{r'} nums[i] i=mid+1rnums[i]最大。

这里我们作符号简化处理,

l m s u m ( l , r ) lmsum(l,r) lmsum(l,r)表示数组中以 l l l开始的所有子数组中最大的子数组和;

l m s u m ( l , r ) = { ∑ i = l r ′ n u m s [ i ]   ∣   l ≤ r ′ ≤ r } lmsum(l,r) = \left\{ \sum_{i=l}^{r'}nums[i] \ \Bigg | \ l \le r' \le r\right\} lmsum(l,r)= i=lrnums[i]   lrr

r m s u m ( l , r ) rmsum(l,r) rmsum(l,r)表示数组中以 r r r结尾的所有子数组 中最大子数组和。
r m s u m ( l , r ) = { ∑ i = l ′ r n u m s [ i ]   ∣   l ≤ l ′ ≤ r } rmsum(l,r)=\left\{ \sum_{i=l'}^{r}nums[i] \ \Bigg |\ l \le l' \le r \right\} rmsum(l,r)={i=lrnums[i]   llr}

综合上面的分析,可以得到

m s u m ( l , r ) = max ⁡ { m s u m ( l , m i d ) , m s u m ( m i d + 1 , r ) , r m s u m ( l , m i d ) + l m s u m ( m i d + 1 , r ) } msum(l,r) =\\ \max \left\{ msum(l,mid), msum(mid+1,r),\\ rmsum(l,mid)+lmsum(mid+1,r)\right\} msum(l,r)=max{msum(l,mid),msum(mid+1,r),rmsum(l,mid)+lmsum(mid+1,r)}
前面两个子区间的最大子数组和直接递归即可。

后面的 l m s u m ( m i d + 1 , r )   r m s u m ( l , m i d ) lmsum(mid+1,r)\ rmsum(l,mid) lmsum(mid+1,r) rmsum(l,mid)怎么求呢?

这里有两种方式:

第一种直接枚举;

对于 l m s u m ( m i d + 1 , r ) lmsum(mid+1,r) lmsum(mid+1,r), 我们可以从左往右枚举 r ′ , m i d + 1 ≤ r ′ ≤ r r', mid+1 \le r' \le r r,mid+1rr, 求得区间和 s u m ( m i d + 1 , r ′ ) sum(mid+1,r') sum(mid+1,r),取最大值即可;

对于 r m s u m ( l , m i d ) rmsum(l,mid) rmsum(l,mid)同理,只是我们从右往左逆序枚举 l ′ l' l,求得区间和 s u m ( l ′ , m i d ) sum(l',mid) sum(l,mid), 取最大值即可。

下面是不那么重要的代码

class Solution {
public:

    int getMaxSubArray(const vector<int> &nums, int l, int r)
    {
        if ( l == r)
            return nums[l];

        int mid = l + ((r -l) >> 1);
        

        int lsum = nums[mid];
        int tsum = nums[mid];
        for (int i = mid - 1; i >= l; --i) {
            tsum += nums[i];
            lsum = max( lsum, tsum );
        }

        int rsum = nums[mid + 1];
        tsum = nums[mid + 1];
        for (int i = mid + 2; i <= r; ++i) {
            tsum += nums[i];
            rsum = max( rsum, tsum);
        }

        int ans = lsum + rsum;
        ans = max(ans, getMaxSubArray(nums, l       , mid ));
        ans = max(ans, getMaxSubArray(nums, mid + 1 , r      ));
        
        return ans;
    }

    int maxSubArray(vector<int>& nums) {
        return getMaxSubArray( nums, 0, static_cast<int>(nums.size()) - 1);    
    }
};

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度 O ( log ⁡ n ) O(\log n) O(logn)

第二种方式是递归的,对于 l m s u m ( l , r ) lmsum(l,r) lmsum(l,r)我们同样可以根据左右区间求得,

以左端点为开端的最大子数组要么是在左子区间,要么是左子区间加上右子区间的以左端点为开端的最大子数组。

这里我们就需要数组的区间和了,我们以 s u m ( l , r ) sum(l,r) sum(l,r)表示 ∑ i = l r n u m s [ i ] \sum_{i=l}^{r}nums[i] i=lrnums[i]

叙述起来有点绕,用符号表示很简单也就是。

l m s u m ( l , r ) = max ⁡ { l m s u m ( l , m i d ) , s u m ( l , m i d ) + l m s u m ( m i d + 1 , r ) } lmsum(l,r) =\\\max\left\{ lmsum(l,mid), \\sum(l,mid)+lmsum(mid+1,r)\right\} lmsum(l,r)=max{lmsum(l,mid),sum(l,mid)+lmsum(mid+1,r)}

同理

r m s u m ( l , r ) = max ⁡ { r m s u m ( m i d + 1 , r ) , s u m ( m i d + 1 , r ) + r m s u m ( l , m i d ) } rmsum(l,r) =\\\max \left\{ rmsum(mid+1,r), \\ sum(mid+1,r) +rmsum(l,mid)\right\} rmsum(l,r)=max{rmsum(mid+1,r),sum(mid+1,r)+rmsum(l,mid)}

因此,对于一个区间,我们需要维护四个值

  • t s u m tsum tsum:区间和
  • l m s u m lmsum lmsum: 以左端点为起点的所有子数组的最大和
  • r m s u m rmsum rmsum:以右端点为终点的所有子数组的最大和
  • s u m sum sum: 区间的最大子数组和

用一个结构体进行存储

代码如下,差不多就是官解同款了!

class Solution {
public:

    struct status {
      
        status(int msum_,int lmsum_, int rmsum_,int tsum_):msum( msum_), lmsum( lmsum_), rmsum( rmsum_ ), tsum(tsum_) 
        {}
        int msum;
        int lmsum;
        int rmsum;
        int tsum;
    };


    status getMaxSubArray(const vector<int> &nums, int l, int r)
    {
        if ( l == r)
            return status{ nums[l], nums[l], nums[l], nums[l]};

        int mid = l + ( (r - l) >> 1);

        auto ls = getMaxSubArray( nums, l, mid);
        auto rs = getMaxSubArray( nums, mid + 1, r);

        status s = ls;

        s.tsum = ls.tsum + rs.tsum;
        s.lmsum = max(ls.lmsum, ls.tsum + rs.lmsum);
        s.rmsum = max(rs.rmsum, ls.rmsum + rs.tsum);
        s.msum  = max( ls.rmsum + rs.lmsum, max(ls.msum, rs.msum) );


        return s;
    }

    int maxSubArray(vector<int>& nums) {
        return getMaxSubArray( nums, 0, static_cast<int>(nums.size()) - 1).msum;    
    }
};

时间复杂度 O ( n ) O(n) O(n), 空间复杂度 O ( log ⁡ n ) O(\log n) O(logn)
不太清楚怎么分析出 O ( n ) O(n) O(n)的,有问题找官解。

2.4 前缀和+贪心

这个解法在评论区中看到的0x3f的解法;

由于本质是找一个区间和 [ l ′ , r ′ ] [l',r'] [l,r]使得它的和在 [ l , r ] [l,r] [l,r]的所有子区间中最大。

我们可以累加得到前缀和 p r e s u m presum presum,从而将问题从找区间最大和转化为找到一对 i , j i,j i,j使得 p r e s u m [ j ] − p r e s u m [ i ] presum[j] -presum[i] presum[j]presum[i]最大。

区间和最大变成数组中后前值差值最大。

而这个问题其实就是lc121 买卖股票的最佳时机。

也就是贪心的处理了,唯一不同的至少要买卖一次。

先给出两次遍历的代码,先计算前缀和,再贪心处理。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        
        int pre = 0;
        for (auto &v: nums) {
            pre += v;
            v = pre;
        }

        
        int n = nums.size();
        int mn = 0;
        int ans = nums[0];
        
        for (int i = 0;i < n; ++i) {
            ans = max(ans, nums[i] - mn);
            mn  = min(mn, nums[i]);
        }

        return  ans;
    }
};

边计算前缀和,边贪心处理,注意对 m i n _ p r e s u m min\_{presum} min_presum的边界处理。

class Solution {
public:

    int maxSubArray(vector<int>& nums) {
        
        int n = nums.size();

        int ans = nums[0];
        int mn = 0;
        for (int i = 0;i < n; ++i) {
            if (i > 0)
                nums[i] += nums[i - 1];
            ans = max(nums[i] - mn, ans);
            mn  = min( mn, nums[i]);
        }
        return ans;
    }
};

参考

lc官解
0x3f

你可能感兴趣的:(leetcode_53 最大子数组和)