【C/C++】53. 最大子数组和

303 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情


题目链接:53. 最大子数组和

题目描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

提示:

  • 1nums.length1051 \leqslant nums.length \leqslant 10^5
  • 104 nums[i]104-10^4 \leqslant nums[i] \leqslant 10^4

示例 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

整理题意

题目给定一个整数数组,让我们返回数组中的 连续 子数组和的最大值。

要求子数组至少包含一个元素,也就是子数组不能为空。

解题思路分析

首先观察题目数据范围为 10510^5 以内,如果暴力枚举所有连续子数组,时间复杂度为 O(n2)O(n^2),会超时 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] 单独成为一段更好。

需要注意连续子数组不能为空,也就是如果数组中的元素都是负数,答案是可以为负数的。

具体实现

  1. 定义 dp[i] 为以第 i 个数结尾的「连续子数组的最大和」
  2. 转移方程为:dp[i]=max(dp[i1]+nums[i],nums[i])dp[i] = max(dp[i−1] + nums[i], nums[i])
  3. 初始化边界 dp[0] = nums[0];
  4. 记录 dp[i]0 <= i < n)中的最大值并返回。

由于 dp[i] 只和 dp[i - 1] 相关,于是我们可以只用一个变量 dp 来维护对于当前 dp[i]dp[i - 1] 的值是多少,从而让空间复杂度降低到 O(1)O(1),这有点类似 「滚动数组」 的思想。

复杂度分析

  • 时间复杂度:O(n)O(n),其中 n 为 nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
  • 空间复杂度:O(1)O(1)。只需要常数空间。

代码实现

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] 分治下去出现的所有子区间的信息都用堆式存储的方式记忆化下来,即建成一颗真正的树之后,我们就可以在 O(logn)O(\log n) 的时间内求到任意区间内的答案,我们甚至可以修改序列中的值,做一些简单的维护,之后仍然可以在 O(logn)O(\log n) 的时间内求到任意区间内的答案,对于大规模查询的情况下,「分治法」的优势便体现了出来。这棵树的数据结构就是 线段树
  • 测试结果:

53.png

结束语

很多人都渴望自己能变得更出色,但无论你想要做成什么事,都没有捷径。所有看似风光的背后,都藏着无尽的汗水;所有令人羡慕的成就背后,都是不一般的自律。生活终会奖赏一个自律的人。新的一天,加油!