携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情
题目链接:53. 最大子数组和
题目描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
提示:
示例 1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入: nums = [1]
输出: 1
示例 3:
输入: nums = [5,4,-1,7,8]
输出: 23
整理题意
题目给定一个整数数组,让我们返回数组中的 连续 子数组和的最大值。
要求子数组至少包含一个元素,也就是子数组不能为空。
解题思路分析
首先观察题目数据范围为 以内,如果暴力枚举所有连续子数组,时间复杂度为 ,会超时 TLE。
由于题目要求返回的是 连续 子数组,我们可以 枚举连续子数组的右端点位置 来寻找和最大的连续子数组。我们用 dp[i] 代表以第 i 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是 dp[i](0 <= i < n)中的最大值,因此我们只需要求出每个位置的 dp[i],然后返回 dp 数组中的最大值即可。
定义了 dp[i] 之后考虑转移方程,由于题目要求子数组连续,所以我们只需考虑对于当前的 nums[i] 和 上一个数 nums[i - 1],考虑让 nums[i] 加入 nums[i - 1] 还是单独成为一段,由于题目要求最大和,所以这取决于加入 nums[i - 1] 和单独成为一段的最大和哪一个更大。
那么可知对于前面以 nums[i - 1] 结尾的「连续子数组的最大和」 dp[i - 1] 如果为负数或零(和小于等于零的)都是对 dp[i] 都是没有贡献的,我们直接让 nums[i] 单独成为一段更好。
需要注意连续子数组不能为空,也就是如果数组中的元素都是负数,答案是可以为负数的。
具体实现
- 定义
dp[i]为以第i个数结尾的「连续子数组的最大和」 - 转移方程为:
- 初始化边界
dp[0] = nums[0]; - 记录
dp[i](0 <= i < n)中的最大值并返回。
由于
dp[i]只和dp[i - 1]相关,于是我们可以只用一个变量 dp 来维护对于当前dp[i]的dp[i - 1]的值是多少,从而让空间复杂度降低到 ,这有点类似 「滚动数组」 的思想。
复杂度分析
- 时间复杂度:,其中
n为nums数组的长度。我们只需要遍历一遍数组即可求得答案。 - 空间复杂度:。只需要常数空间。
代码实现
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
//初始化边界
int dp = nums[0];
//ans记录最大值
int ans = nums[0];
//递推求最大值
for(int i = 1; i < n; i++){
dp = max(dp + nums[i], nums[i]);
ans = max(ans, dp);
}
return ans;
}
};
总结
- 该题核心在于 枚举连续子数组的右端点位置 ,从而定义
dp[i]代表以第i个数结尾的「连续子数组的最大和」为关键步骤。 - 考虑转移方程时,由于
i时从小到大递推的,在求dp[i]的时候dp[i - 1]已经求出,从而dp[i - 1]是dp[i]的子问题。 - 该题还有更为进阶的精妙 分治法 求解。「分治法」相较于「动态规划」来说,时间复杂度相同,但是因为使用了递归,并且维护了四个信息的结构体,运行的时间略长,空间复杂度也不如动态规划优秀,而且难以理解。对于这道题而言,确实是「动态规划」更优。
- 「分治法」存在的意义在于它不仅可以解决区间
[0, n-1],还可以用于解决任意的子区间[l,r]的问题。如果我们把[0, n-1]分治下去出现的所有子区间的信息都用堆式存储的方式记忆化下来,即建成一颗真正的树之后,我们就可以在 的时间内求到任意区间内的答案,我们甚至可以修改序列中的值,做一些简单的维护,之后仍然可以在 的时间内求到任意区间内的答案,对于大规模查询的情况下,「分治法」的优势便体现了出来。这棵树的数据结构就是 线段树。 - 测试结果:
结束语
很多人都渴望自己能变得更出色,但无论你想要做成什么事,都没有捷径。所有看似风光的背后,都藏着无尽的汗水;所有令人羡慕的成就背后,都是不一般的自律。生活终会奖赏一个自律的人。新的一天,加油!