给定一个数组,让你求最大子数组和;
所谓的子数组,就是数组中连续的一段。
非常经典的一道题目,值得反复把玩啊!!!
首先我们想想怎么枚举子数组。
我们可以固定子数组的左端点,再依次处理子数组的右端点。
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)
我们可以定义 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[n−1]+nums[n−1],dp[n−1]>0nums[n−1],dp[n−1]≤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]},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)
代码和动态规划的代码几乎一样,但是背后的思想却是不一样的。
就像评论区说的渣男渣女一样,之前的数组和如果小于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;
}
};
分治的思路跟归并排序、线段树的思想差不多。
首先分治的核心是把大的问题分成两个规模差不多一样大的子问题,
所以我们先要看怎么将大数组的最大子数组和给转换为小数组的
最大子数组和。
我们假设,数组范围为 [ 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=l′∑r′nums[i] l≤l′,r′≤r⎭ ⎬ ⎫
我们假设最大子数组最终的区间为 [ 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 l′≤mid∩r′≥mid+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=l′midnums[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+1r′nums[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=l∑r′nums[i] l≤r′≤r⎭ ⎬ ⎫
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=l′∑rnums[i] l≤l′≤r}
综合上面的分析,可以得到
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+1≤r′≤r, 求得区间和 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)}。
因此,对于一个区间,我们需要维护四个值
用一个结构体进行存储
代码如下,差不多就是官解同款了!
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)的,有问题找官解。
这个解法在评论区中看到的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