题目来源
题目描述
题目解析
先看数据量
1 <= nums.length <= 10^5
:这意味着我们最多只能使用O(N)的解法
暴力法(超时)
双层for循环,第一层for设置起始位置,第二层for遍历数组寻址最大值
int maxSubArray(vector<int> & nums){
int max = INT_MIN;
for (int i = 0; i < nums.size(); ++i) {
int sum = 0;
for (int j = i; j < nums.size(); ++j) {
sum += nums[j];
if(sum > max){
max = sum;
}
}
}
return max;
}
超出时间限制
动态规划
第二次做
思路一
对于子数组、子串之类的问题,我们应该这样想:
- 如果子数组必须以0位置结尾,它往左扩大到什么程度,能让累加和最大
- 如果子数组必须以1位置结尾,它往左扩大到什么程度,能让累加和最大
- …
把所有的以i位置结尾的最大累加和求出来,答案就是它们中最大的。
思考:必须以i位置结尾的答案可能性有哪些(或者说它往左扩有几种方法)
- 完全不左扩
- 答案:nums[i]
- 要往左扩
- 问题是要扩几步?
-
i
−
1
i - 1
i−1结尾的时候扩出来的最好决定了当前能扩出来的最好
- sum[i] = nums[i] + sum[i - 1]
- 子数组必须是连续的, i − 1 i - 1 i−1再往前,就会令sum[i-1]变小,从而sum[i]变小
那么区分这两个呢?思考它往左扩要被哪些因素影响
- 如果它是第0个数,那么不能左扩
- 如果它不是第0个数,那么它有可能左扩,也可能不动
- 如果当前数是负数、sum[i-1]是负数:sum[i] = nums[i],完全不左扩
- 如果当前数是正数,sum[i-1]是负数:sum[i] = nums[i],完全不左扩
- 如果当前数是负数、sum[i-1]是0:sum[i] = nums[i],动和不动没有意义
- 如果当前数是正数、sum[i-1]是0:sum[i] = nums[i],动和不动没有意义
- 如果当前数是负数,sum[i-1]是正数:sum[i] = nums[i] + sum[i-1]
- 如果当前数是正数、sum[i-1]是整数:sum[i] = nums[i] + sum[i-1]
- 综上:
- 如果sum[i-1]<= 0,sum[i] = nums[i]
- 否则:sum[i] = nums[i] + sum[i-1]
综上:
- 定义sum[i]为必须以i位置结尾的时候最大累加和是多少
- sum数组长度: i取值
0~n-1
,所以sum最大长度为数组长度 - 返回值:sum[i]中最大的哪一个就是答案
- 因为sum[i]只依赖i-1和i,所以只需要几个变量滚动下去就行了
不优化的写法:
class Solution{
public:
int maxSum(vector<int> nums){
if(nums.empty()){
return 0;
}
int N = nums.size();
std::vector<int> sum(N, 0); // 子数组必须以i位置结尾的情况下,累加和最大是多少?
sum[0] = nums[0];
for (int i = 1; i < N; ++i) {
if(sum[i - 1] <= 0){
sum[i] = nums[i];
}else{
sum[i] = nums[i] + sum[i - 1];
}
}
int max = INT32_MIN;
for (int i = 0; i < N; ++i) {
max = std::max(max, sum[i]);
}
return max;
}
};
思路二
思考:必须以i位置结尾的答案可能性有哪些(或者说它往左扩有几种方法)
- 自己单独
- 自己和上面的那个联通
上面两种选择一个累加和最大的
int maxSum1(vector<int> nums){
if(nums.empty()){
return 0;
}
int N = nums.size();
int max = nums[0];
int pre = nums[0];
for (int i = 1; i < N; ++i) {
int curr = std::max(nums[i], nums[i] + pre);
max = std::max(max, curr);
pre = curr;
}
return max;
}
对数器
std::default_random_engine e;
void generateRandom(int maxLen, int minValue, int maxValue, std::vector<int> &arr){
std::uniform_int_distribution<int> distS(1, maxLen);
std::uniform_int_distribution<int> distV(minValue, maxValue);
std::uniform_real_distribution<double> distf;
int size = distS(e);
arr.resize(size);
for (int i = 0; i < size; ++i) {
arr[i] = distV(e);
}
}
int main(){
Solution a;
e.seed(time(NULL));
int maxLen = 100, minValue = -1000, maxValue = 100000;
for (int i = 0; i < 100000; ++i) {
std::vector<int> arr;
generateRandom(maxLen, minValue, maxValue, arr);
if(a.maxSum(arr) != a.maxSum1(arr)){
printf("error\n");
return 0;
}
}
printf("ok\n");
return 0;
}
第一次做
关键点1:理解题意
题目要我们找出最大的连续子数组的值是多少,【连续】是关键字,连续很重要,不是子序列。
题目只要求返回结果,不要求得到最大的连续子数组是哪一个。这样的问题通常可以使用【动态规划】来解决。
关键点2:如何定义子问题
思路:把不确定的因素确定下来,进而把子问题定义清楚,把子问题定义得简单。动态规划的思想是通过仅仅一个个简单的问题,进而把简单的问题的题组成了复杂的问题的解
我们不知道和最大的连续子数组一定会选哪一个数,那么我们可以求出所有经过输入数组的某一个数的连续子数组的最大和。
例如,示例 1 输入数组是 [-2,1,-3,4,-1,2,1,-5,4] ,我们可以求出以下子问题:
- 子问题 1:经过 -2的连续子数组的最大和是多少;
- 子问题 2:经过 1的连续子数组的最大和是多少;
- 子问题 3:经过 −3 的连续子数组的最大和是多少;
- 子问题 4:经过 4 的连续子数组的最大和是多少;
- 子问题 5:经过 −1 的连续子数组的最大和是多少;
- 子问题 6:经过 2 的连续子数组的最大和是多少;
- 子问题 7:经过 1 的连续子数组的最大和是多少;
- 子问题 8:经过 −5 的连续子数组的最大和是多少;
- 子问题 9:经过 4 的连续子数组的最大和是多少
一共 9 个子问题,这些子问题之间的联系并没有那么好看出来,这是因为 子问题的描述还有不确定的地方(这件事情叫做「有后效性」)
例如「子问题 3」:经过 -3的连续子数组的最大和是多少。
「经过 -3−3 的连续子数组」我们任意举出几个:
- [-2,1,-3,4] ,-3是这个连续子数组的第 3 个元素;
- [1,-3,4,-1] ,-3是这个连续子数组的第 2 个元素;
- …
我们不确定的是:-3是连续子数组的第几个元素。那么我们就把-3定义成连续子数组的最后一个元素。在新的定义下,我们列出子问题如下:
- 子问题 1:以 − 2 -2 −2结尾的连续子数组的最大和是多少;
- 子问题 2:以 1结尾的连续子数组的最大和是多少;
- 子问题 3:以 -3 结尾的连续子数组的最大和是多少;
- 子问题 4:以 4结尾的连续子数组的最大和是多少;
- 子问题 5:以 −1 结尾的连续子数组的最大和是多少;
- 子问题 6:以 2 结尾的连续子数组的最大和是多少;
- 子问题 7:以 1 结尾的连续子数组的最大和是多少;
- 子问题 8:以 −5 结尾的连续子数组的最大和是多少;
- 子问题 9:以 4 结尾的连续子数组的最大和是多少。
我们加上了【结尾的】,这些子问题之间就有了连续。我们单独看子问题
(1) 子问题 1:以
−
2
-2
−2结尾的连续子数组的最大和是多少;
- 以-2结尾的连续子数组有【-2】,因此最大和是-2
(2)子问题 2:以 1结尾的连续子数组的最大和是多少;
- 以1结尾的连续子数组有【-2,1】,sum=-1 (丢弃)
- 以1结尾的连续子数组有【1】,sum=1
- 因此【子问题2】的答案是1
(3)子问题 3:以 -3 结尾的连续子数组的最大和是多少;
- 以-3结尾的连续子数组有【-2,1,-3】,sum=0
- 以-3结尾的连续子数组有【1,-3】,sum=-2
- 以-3结尾的连续子数组有【-3】,sum=-3
- 因此【子问题3】的答案是0
(4)子问题 4:以 4结尾的连续子数组的最大和是多少;
- 以4结尾的连续子数组有【-2,1,-3,4】,sum=4
- 以4结尾的连续子数组有【1,-3,4】,sum=2
- 以4结尾的连续子数组有【-3,4】,sum=1
- 因此【子问题3】的答案是4
…
可以发现,如果编号为i的子问题的结果是负数或者0,那么编号为i+1的子问题就可以把编号为i的子问题的结果舍弃掉。这是因为:
- 一个数a加上负数的结果比a更小
- 一个数a加上0的结果不会比a更大
- 而子问题的定义必须以一个数为结尾,因此如果子问题i的结构是负数或者0,那么子问题i+1的da’an就是以nums[i]结尾的那个数。
写动态方程
接下来我们按照编写动态规划题解的步骤,把「状态定义」「状态转移方程」「初始化」「输出」「是否可以空间优化」全都写出来。
(1)定义状态(定义子问题)
dp[i]
:表示以nums[i]
结尾的连续子数组的最大和
(2)状态转移方程
- 根据状态的定义,由于
nums[i]
一定会被选取,并且以nums[i]
结尾的连续子数组与以nums[i - 1]
结尾的连续子数组只相差一个元素nums[i]
。 - 假设数组
nums
的值全都严格大于 0,那么一定有dp[i] = dp[i - 1] + nums[i]
。 - 可是
dp[i - 1]
有可能是负数,于是分类讨论:- 如果
dp[i - 1] > 0
,那么可以把nums[i]
直接接在dp[i - 1]
表示的那个数组的后面,得到和更大的连续子数组; - 如果
dp[i - 1] <= 0
,那么nums[i]
加上前面的数dp[i - 1]
以后值不会变大。于是dp[i]
「另起炉灶」,此时单独的一个 nums[i] 的值,就是 dp[i]。
- 如果
于是状态转移方程如下:
记为「状态转移方程 1」。
状态转移方程还可以这样写,反正求的是最大值,也不用分类讨论了,就这两种情况,取最大即可,因此还可以写出状态转移方程如下:
记为「状态转移方程 2」
(3)思考初始值
- dp[0] 根据定义,只有 1 个数,一定以 nums[0] 结尾,因此 dp[0] = nums[0]
(4)思考输出
- 这里状态的定义不是题目中的问题的定义,不能直接将最后一个状态返回回去;
- 这个问题的输出是把所有的 dp[0]、dp[1]、……、dp[n - 1] 都看一遍,取最大值。
(5)可以优化空间吗
- 根据「状态转移方程」,dp[i] 的值只和 dp[i - 1] 有关,因此可以使用「滚动变量」的方式将代码进行优化。
小结
- 思想:要么是加了前面的大,要么是不加大
- 确定dp数组以及下标含义:用
ni
表示nums[i]
,用dp(i)
表示以第i个数结尾的连续子数组的最大和 - 确定递推公式:如何求
dp(i)
呢?可以考虑ni
单独成为一段还是加入dp(i - 1)
对应的那一段。这取决于ni
以及dp[i - 1] + ni
的大小。因此dp[i] = std::max(dp[i - 1] + ni, ni)
- dp数组如何初始化:f(0) = nums[0]
- 如何遍历顺序:考虑到
dp(i)只和dp[i -1]
相关,所以从前往后遍历 - 空间优化:我们可以只用一个pre来维护对于当前dp[i]的dp[i-1]的值是多少,从而让空间复杂度降低到O(1)
- 小结:当使用动态规划只需要一个数而不需要整个dp数组,都可以用变量来进行空间优化
解答题目
(1)不空间优化
之前的题,是已经选好了dp[i-1],然后来考虑要不要选第i个值。这道题是一定要选第i个值,然后再考虑要不要选第dp[i-1]。
int maxSubArray(vector<int> & nums){
int len = nums.size();
std::vector<int> dp (len, 0); // dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
dp[0] = nums[0];
for (int i = 1; i < nums.size(); ++i) {
if(dp[i - 1] > 0 ){ // 要之前的值
dp[i] = dp[i - 1] + nums[i];
}else{
dp[i] = nums[i]; // 不要之前的值
}
}
int res = dp[0];
for (int j = 1; j < dp.size(); ++j) {
if(dp[j] > res){
res = dp[j];
}
}
return res;
}
(2)空间优化
int maxSubArray(vector<int> & nums){
int pre = 0; //用pre来维护当前dp[i]的dp[i-1]的值是多少
int res = nums[0];
for (int num : nums) {
pre = std::max(pre + num, num); //判断dp[i - 1]是否要加到当前树上
res = std::max(res, pre); //获取最大值
}
return res;
}
贪心
贪心贪的是哪里呢?
- 如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!
- 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
- 全局最优:选取最大“连续和”
- 局部最优的情况下,
思路:
-
遍历nums,从头开始用sum累积,如果sum一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积sum了,因为已经变为负数的sum,只会拖累总和。
-
这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
-
区间终止位置不用调整么? 如何才能得到最大“连续和”呢?
-
区间的终止位置,其实就是如果count取到最大值了,及时记录下来了。例如如下代码:
if (count > result) result = count;
- 这样相当于是用result记录最大子序和区间和(变相的算是调整了终止位置)。
- 也就是说,在迭代的过程中,我们要用result来不断维持当前的最大子序和,因为count的值是在不断更新的,所以我们要即使记录下它的最大值
红色的起始位置就是贪心每次取count为正数的时候,开始一个区间的统计。
说明:
- 当数组全是负数时,在nums不断迭代的过程中,早已将最大值不断传给result, 即使sum一直是负数被不断置零也不用担心, result还是会记录下最大的那个负数.
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums[i];
if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置)
result = count;
}
if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
}
return result;
}
};
分治法
思想
- 取数组中心点为中心,最大子序列要么全在中心左边,要么全在中心右边,要么跨中心。
- 因此可以分三种情况考虑:跨中心、在分治成中心店左侧和右侧的最大子序和问题
也就是说,连续子序列的最大和主要由三部分得到:
- 第 1 部分:子区间[left,mid]
- 第 2 部分:子区间 [mid + 1, right];
- 第 3 部分:包含子区间[mid,mid + 1]的子区间,即nums[mid] 与 nums[mid + 1] 一定会被选取。
对这三个部分求最大值即可。
说明:考虑第3部分跨越两个区间的连续子数组的时候,由于nums[mid] 与 nums[mid + 1]一定会被选取,所以可以从中间向两边扩散,扩散到底选出最大值
class Solution{
int maxCrossingSum(std::vector<int> &nums, int left, int mid, int right){
// 一定会包含 nums[mid] 这个元素
int sum = 0;
int leftSum = INT32_MIN;
// 左半边包含 nums[mid] 元素,最多可以到什么地方
// 走到最边界,看看最值是什么
// 计算以 mid 结尾的最大的子数组的和
for (int i = mid; i >= left; --i) {
sum += nums[i];
if(sum > leftSum){
leftSum = sum;
}
}
sum = 0;
int rightSum = INT32_MIN;
// 右半边不包含 nums[mid] 元素,最多可以到什么地方
// 计算以 mid+1 开始的最大的子数组的和
for (int i = mid + 1; i <= right; ++i) {
sum += nums[i];
if(sum > rightSum){
rightSum = sum;
}
}
return leftSum + rightSum;
}
int process(std::vector<int> &nums, int left, int right){
if(left == right){
return nums[left];
}
int mid = left + (right - left) / 2;
return std::max(process(nums, left, mid),
std::max(process(nums, mid + 1, right),
maxCrossingSum(nums, left, mid, right)));
}
public:
int maxSubArray(std::vector<int> nums){
int len = nums.size();
if(len == 0){
return 0;
}
return process(nums, 0, len - 1);
}
};
思路
类似[线段树求最长公共上升子序列问题]的pushUp操作。
class Solution{
struct Status{
int lSum, rSum, mSum, iSum;
};
Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
int lSum = max(l.lSum, l.iSum + r.lSum);
int rSum = max(r.rSum, r.iSum + l.rSum);
int mSum = max(max(l.mSum, r.mSum), l.rSum + r.lSum);
return (Status) {lSum, rSum, mSum, iSum};
};
Status get(vector<int> &a, int l, int r){
if(l == r){
return (Status) {a[l], a[l], a[l], a[l]};
}
int m = (l + r) >> 1;
Status lSub = get(a, l, m);
Status rSub = get(a, m + 1, r);
return pushUp(lSub, rSub);
}
public:
int maxSubArray(vector<int>& nums) {
return get(nums, 0, nums.size() - 1).mSum; // [l, r]内的最大子段和
}
};
类似题目
题目 | 思路 |
---|---|
leetcode:53. 子数组最大累加和是多少 (无序整数数组)maximum-subarray | 动态规划、线段和、贪心 |
leetcode:1186. 删除一次得到子数组最大和(无序整数数组) maximum-subarray-sum-with-one-deletion | 动态规划 |
leetcode:152. 子数组最大乘积是多少 (无序整数数组)Maximum Product Subarray | 动态规划(最小值 * 最小值可能变得很大) |
leetcode:1749. 子数组最大累加和绝对值是多少(无序整数数组) Maximum Absolute Sum of Any Subarray | 前缀和 |
leetcode:209. 和>=k的最短子数组的长度(无序正数数组)Minimum Size Subarray Sum | 前缀和 + 二分、滑动窗口 |
leetcode:862. 和>=K的最短子数组长度(无序整数数组) | |
leetcode:325. 和==k的最长子数组的长度(无序整数数组) | 前缀和 + 哈希 |
algorithm:和<=k的最长子数组的长度(无序整数数组) | 前缀和 + 枚举所有子序列 |
leetcode:560. 和== K 的子数组的个数 subarray-sum-equals-k | 前缀和+哈希:计算完包括了当前数前缀和之后,我们去查一查在当前数之前,有多少个前缀和等于preSum - k呢? |
leetcode:713. 乘积 < K 的子数组个数subarray-product-less-than-k | 滑动窗口 |
leetcode:978. 最大湍流子数组的长度 Longest Turbulent Subarray | |
leetcode:697. 数组的度 degree-of-an-array |