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(n2)O(n^2)O(n2)
2.2 动态规划
我们可以定义dp[n]dp[n]dp[n]为前nnn个数组的最大子数组和。
那么状态转移方程为
dp[n]={dp[n−1]+nums[n−1],dp[n−1]>0nums[n−1],dp[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[n−1]+nums[n−1],dp[n−1]>0nums[n−1],dp[n−1]≤0
最终的最大子数组和为
max{dp[i]},1≤i≤arr.size\max \{ dp[i]\} , 1 \le i \le arr.size max{dp[i]},1≤i≤arr.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,mid],[mid+1,r][l,mid],[mid+1,r][l,mid],[mid+1,r]。
再令msum(l,r)msum(l,r)msum(l,r)表示: [l,r][l,r][l,r]数组的最大子数组和。
msum(l,r)=max{∑i=l′r′nums[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=l′∑r′nums[i] l≤l′,r′≤r⎭⎬⎫
我们假设最大子数组最终的区间为[l′,r′][l',r'][l′,r′].
我们很容易的想到msum(l,mid)msum(mid+1,r)msum(l,mid)\ msum(mid+1,r)msum(l,mid) msum(mid+1,r)
可能是msum(l,r)msum(l,r)msum(l,r)的候选值。用更通俗的话来讲,子数组的最大子数组
和可能是数组的最大子数组和。
分别对应了[l′,r′]⊆[l,mid][l',r'] \subseteq [l,mid][l′,r′]⊆[l,mid]和[l′,r′]⊆[mid+1,r][l',r'] \subseteq [mid+1,r][l′,r′]⊆[mid+1,r]。
除此之外,我们还有一种情况,那就是
最大子数组的区间跨越了两个区间,也就是l′≤mid∩r′≥mid+1l'\le mid \cap r' \ge mid+1l′≤mid∩r′≥mid+1
此时我们需要分到两个子区间进行处理,也就是[l′,mid][mid+1,r′][l',mid]\ [mid+1,r'][l′,mid] [mid+1,r′]。
我们需要找到左下标l′∈[l,mid]l'\in[l,mid]l′∈[l,mid]使得,∑i=l′midnums[i]\sum_{i=l'}^{mid} nums[i]∑i=l′midnums[i]最大;
同理我们需要找到右下标r′∈[mid+1,r]r'\in[mid+1,r]r′∈[mid+1,r]使得,∑i=mid+1r′nums[i]\sum_{i=mid+1}^{r'} nums[i]∑i=mid+1r′nums[i]最大。
这里我们作符号简化处理,
令lmsum(l,r)lmsum(l,r)lmsum(l,r)表示数组中以lll开始的所有子数组中最大的子数组和;
lmsum(l,r)={∑i=lr′nums[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=l∑r′nums[i] l≤r′≤r⎭⎬⎫
rmsum(l,r)rmsum(l,r)rmsum(l,r)表示数组中以rrr结尾的所有子数组 中最大子数组和。
rmsum(l,r)={∑i=l′rnums[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=l′∑rnums[i] l≤l′≤r}
综合上面的分析,可以得到
msum(l,r)=max{msum(l,mid),msum(mid+1,r),rmsum(l,mid)+lmsum(mid+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)}
前面两个子区间的最大子数组和直接递归即可。
后面的lmsum(mid+1,r)rmsum(l,mid)lmsum(mid+1,r)\ rmsum(l,mid)lmsum(mid+1,r) rmsum(l,mid)怎么求呢?
这里有两种方式:
第一种直接枚举;
对于lmsum(mid+1,r)lmsum(mid+1,r)lmsum(mid+1,r), 我们可以从左往右枚举r′,mid+1≤r′≤rr', mid+1 \le r' \le rr′,mid+1≤r′≤r, 求得区间和sum(mid+1,r′)sum(mid+1,r')sum(mid+1,r′),取最大值即可;
对于rmsum(l,mid)rmsum(l,mid)rmsum(l,mid)同理,只是我们从右往左逆序枚举l′l'l′,求得区间和sum(l′,mid)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(nlogn)O(n\log n)O(nlogn),空间复杂度O(logn)O(\log n)O(logn)
第二种方式是递归的,对于lmsum(l,r)lmsum(l,r)lmsum(l,r)我们同样可以根据左右区间求得,
以左端点为开端的最大子数组要么是在左子区间,要么是左子区间加上右子区间的以左端点为开端的最大子数组。
这里我们就需要数组的区间和了,我们以sum(l,r)sum(l,r)sum(l,r)表示∑i=lrnums[i]\sum_{i=l}^{r}nums[i]∑i=lrnums[i]
叙述起来有点绕,用符号表示很简单也就是。
lmsum(l,r)=max{lmsum(l,mid),sum(l,mid)+lmsum(mid+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)}。
同理
rmsum(l,r)=max{rmsum(mid+1,r),sum(mid+1,r)+rmsum(l,mid)}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)}。
因此,对于一个区间,我们需要维护四个值
- tsumtsumtsum:区间和
- lmsumlmsumlmsum: 以左端点为起点的所有子数组的最大和
- rmsumrmsumrmsum:以右端点为终点的所有子数组的最大和
- sumsumsum: 区间的最大子数组和
用一个结构体进行存储
代码如下,差不多就是官解同款了!
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(logn)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]的所有子区间中最大。
我们可以累加得到前缀和presumpresumpresum,从而将问题从找区间最大和转化为找到一对i,ji,ji,j使得presum[j]−presum[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;}
};
边计算前缀和,边贪心处理,注意对min_presummin\_{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