1. 题目来源
链接:53. 最大子序和
进阶力扣,字节常见的两题组合拳:面试题 17.24. 最大子矩阵
进阶题:
官方非常好的题解:连续子数组的最大和
2. 题目解析
这题真的是绝了!
显然,暴力是
O
(
n
2
)
O(n^2)
O(n2) 的。dp
能做到
O
(
n
)
O(n)
O(n) 时间,
O
(
1
)
O(1)
O(1) 空间。一般的分治时间是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),空间
O
(
l
o
g
n
)
O(logn)
O(logn)。本文参考题解给出的分治思想,能做到
O
(
n
)
O(n)
O(n) 的时间,
O
(
l
o
g
n
)
O(logn)
O(logn) 的空间,真是绝了!
变量写法远比数组简单,数组这边界情况真的麻烦…关键还是写的太烂了第一次…本文
补一个 O ( n l o g n ) O(nlogn) O(nlogn) 的分治。
思路:
- 区间
[l, r]
内的答案可以分为三部分来计算,记mid = l+r >> 1
- 区间
[l, mid]
内的答案(递归计算)。 - 区间
[mid + 1, r]
内的答案(递归计算)。 - 跨越
mid
和mid + 1
的连续子序列。
- 区间
- 其中,第
(3)
部分只需要从mid
开始向l
找连续的最大值,以及从mid+1
开始向r
找最大值即可,在线性时间内可以完成。 - 递归终止条件显然是
l == r
,此时直接返回nums[l]
。
- 时间复杂度:由递归主定理 S ( n ) = 2 S ( n / 2 ) + O ( n ) S(n)=2S(n/2)+O(n) S(n)=2S(n/2)+O(n),解出总时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。或者每一层时间复杂度是 O ( n ) O(n) O(n),总共有 O ( l o g n ) O(logn) O(logn) 层,故总时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)。
- 空间复杂度: O ( l o g n ) O(logn) O(logn)
代码:
// 麻烦的数组写法
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 1) return nums[0];
vector<int> f(nums.size(), INT_MIN);
f[0] = nums[0];
int res = max(f[0], INT_MIN);
for (int i = 1; i < nums.size(); i ++ ) {
f[i] = max(nums[i], f[i - 1] + nums[i]);
res = max(res, f[i]);
}
return res;
}
};
// 简化
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> f(nums.size());
f[0] = nums[0];
int res = f[0];
for (int i = 1; i < nums.size(); i ++ ) {
f[i] = max(nums[i], nums[i] + f[i - 1]);
res = max(res, f[i]);
}
return res;
}
};
// 再简化代码
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> f(nums.size() + 1);
int res = INT_MIN;
for (int i = 0; i < nums.size(); i ++ ) {
f[i + 1] = max(nums[i], f[i] + nums[i]);
res = max(res, f[i + 1]);
}
return res;
}
};
// 滚动数组优化
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 1) return nums[0];
vector<int> f(2, INT_MIN);
f[0] = nums[0];
int res = max(f[0], INT_MIN);
for (int i = 1; i < nums.size(); i ++ ) {
f[i&1] = max(nums[i], f[i - 1&1] + nums[i]);
res = max(res, f[i&1]);
}
return res;
}
};
// 变量写法
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res = INT_MIN;
for (int i = 0, last = 0; i < nums.size(); i ++ ) {
last = nums[i] + max(0, last);
res = max(res, last);
}
return res;
}
};
// 分治写法 O(n)
class Solution {
public:
struct Node {
int sum, s, ls, rs; // 总和,最大子段和,最大前缀,最大后缀
};
Node build(vector<int> &nums, int l, int r) {
if (l == r) {
// 若数组全为负数则t为0,若数组有一个非负数,则结果就为正
int t = max(nums[l], 0); // 允许区间为空的处理方法。最后再特判所有负数情况即可
return {nums[l], t, t, t};
}
int mid = l + r >> 1;
auto L = build(nums, l, mid), R = build(nums, mid + 1, r);
Node res; // 左右区间合并更新数据
res.sum = L.sum + R.sum; // 左右区间总和
res.s = max(max(L.s, R.s), L.rs + R.ls); // 左右区间最大子段和,左区间最大后缀,右区间最大前缀
res.ls = max(L.ls, L.sum + R.ls); // 左区间最大后缀,左区间最大后缀和左区间总和加右区间最大前缀
res.rs = max(R.rs, R.sum + L.rs); // 右区间最大前缀,右区间最大前缀和右区间总和加左区间最大后缀
return res;
}
int maxSubArray(vector<int>& nums) {
int res = INT_MIN;
for (auto e : nums) res = max(res, e); // 判断所有数字均为负数的情况
if (res < 0) return res;
auto t = build(nums, 0, nums.size() - 1);
return t.s;
}
};
// 补充的分治 O(nlogn)
class Solution {
public:
int calc(int l, int r, vector<int>& nums) {
if (l == r)
return nums[l];
int mid = (l + r) >> 1;
int lmax = nums[mid], lsum = 0, rmax = nums[mid + 1], rsum = 0;
for (int i = mid; i >= l; i--) {
lsum += nums[i];
lmax = max(lmax, lsum);
}
for (int i = mid + 1; i <= r; i++) {
rsum += nums[i];
rmax = max(rmax, rsum);
}
return max(max(calc(l, mid, nums), calc(mid + 1, r, nums)), lmax + rmax);
}
int maxSubArray(vector<int>& nums) {
int n = nums.size();
return calc(0, n - 1, nums);
}
};
20210421 更新,要处理最大子矩阵问题…回过头来看 1 维简化版本。博文写的好乱…
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, -1e9);
f[0] = nums[0];
for (int i = 1; i < n; i ++ ) f[i] = max(f[i - 1] + nums[i], nums[i]);
return *max_element(f.begin(), f.end());
}
};
20210421 又更新。
有个比较好的思路,可以预处理前缀和,枚举终点前缀合 i
,维护一个左半段区间的最小前缀和,再用其减去前半段的最小前缀和即为一段连续的区间,也是以 i
结尾的最大子序和,每次更新这个左半段区间的最小前缀和即可。也是一种很形象的思路。
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> f(n + 1);
for (int i = 1; i <= n; i ++ ) f[i] = f[i - 1] + nums[i - 1];
int ans = -1e9, mn = 0;
for (int i = 1; i <= n; i ++ ) ans = max(ans, f[i] - mn), mn = min(mn, f[i]);
return ans;
}
};
20210717 再更新。
每日一题,状态转移为 f[i]=max(f[i-1]+nums[i], nums[i]);
可发现 f[i]
仅有上一层状态有关,故可以利用滚动数组的优化,用单变量代替 f
数组。
注意写法:
- 不要让
f=nums[0]
,否则在单个元素下就直接返回res
了。 i
从 0 开始,上一层的f
进行状态转移,然后对本层f
进行更新。非常精巧的写为f = max(f + nums[i], nums[i]);
。- 仍需要记录以
i
结尾的最大连续子数组和,而不是直接返回f
,这里的f
和f[n-1]
是一样的。每次都会更新,丢到上次的值。所以需要记录状态转移过程中最大的f
。 - 由于本题的代码特殊性,再抽象为
f = max(f, 0) + nums[i];
,f
是否非负,非负的话,这一段一定不需要加入到答案中,但是具体的dp
过程的思考是从f = max(f + nums[i], nums[i]);
过度过来的。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int f = 0;
int res = -1e9;
for (int i = 0; i < n; i ++ ) {
f = max(f + nums[i], nums[i]);
res = max(f, res);
}
return res;
}
};