Leetcode题解:最大子序和
#53. 最大子序和
给定一个整数数组nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
1 | 输入: [-2,1,-3,4,-1,2,1,-5,4] |
如果你已经实现复杂度为O(n)
的解法,尝试使用更为精妙的分治法求解。
#写在前面
美团二面的笔试题,自己当时真的一点思路都没有,没做出来,主要是O(n)
时间复杂度的限制,再加上我脑子里的动态规划一直都是,从i
到j
的最大和,结果就把自己绕在那里出不来了。
但其实,如果当时知道了思路,真的就是一道,如力扣所说的,一道简单难度
的题 😭
#动态规划
思路是用二维数组来存局部的和,这样不必每次都算一次
1 | class Solution { |
但是这个代码,连测试用例都跑不过,超出内存限制。
#降低空间复杂度
首先观察代码,其实d[i][j]
只与d[i][j-1]
有关,所以只需使用一个一维数组,即可满足存储任务。
进而,d[i][j]
本质上只与前一个数有关,那么其实只需用一个值去存前一个值即可。。
1 | class Solution { |
1 | Success: |
通过了是通过了,但是这题本质上是需要O(n)
时间复杂度求解的,很明显,没有达到要求。
#降低时间复杂度
如果前面的数贡献为正的
,那么我们的最大值就是前面的数加上当前的数,如果前面的数是负的
,那么如果当前的值比前面的数还大,那证明前面的“工作”还不如当前一个人的工作来得多,那前面所有人的工作都可以被当前的人所替代。
1 | class Solution { |
1 | Success: |
#官方解答
假设 nums
数组的长度是 n,下标从 0 到 n - 1。
我们用 ai 代表 nums[i]
,用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是:
因此我们只需要求出每个位置的 f(i),然后返回 f
数组中的最大值即可。那么我们如何求 f(i) 呢?我们可以考虑 a_i 单独成为一段还是加入 f(i - 1) 对应的那一段,这取决于 a_i 和 f(i - 1) + a_i 的大小,我们希望获得一个比较大的,于是可以写出这样的动态规划转移方程:
不难给出一个时间复杂度 O(n)、空间复杂度 O(n) 的实现,即用一个 f
数组来保存 f(i) 的值,用一个循环求出所有 f(i)。考虑到 f(i) 只和 f(i - 1) 相关,于是我们可以只用一个变量 pre
来维护对于当前 f(i) 的 f(i - 1) 的值是多少,从而让空间复杂度降低到 O(1),这有点类似「滚动数组」的思想。
1 | class Solution { |
1 | Success: |
#分治
我们定义一个操作 get(a, l, r)
表示查询 a 序列 [l, r] 区间内的最大子段和,那么最终我们要求的答案就是 get(nums, 0, nums.size() - 1)
。如何分治实现这个操作呢?对于一个区间 [l, r],我们取
$$m=\lfloor\frac{l+r}{2}\rfloor$$
,对区间 [l, m] 和 [m + 1, r] 分治求解。当递归逐层深入直到区间长度缩小为 1 的时候,递归「开始回升」。这个时候我们考虑如何通过 [l, m] 区间的信息和 [m + 1, r] 区间的信息合并成区间 [l, r] 的信息。最关键的两个问题是:
- 我们要维护区间的哪些信息呢?
- 我们如何合并这些信息呢?
对于一个区间 [l, r],我们可以维护四个量:
lSum
表示 [l, r] 内以 l 为左端点的最大子段和rSum
表示 [l, r] 内以 r 为右端点的最大子段和mSum
表示 [l, r] 内的最大子段和iSum
表示 [l, r] 的区间和
以下简称 [l, m] 为 [l, r] 的「左子区间」,[m + 1, r] 为 [l, r] 的「右子区间」。我们考虑如何维护这些量呢(如何通过左右子区间的信息合并得到 [l, r] 的信息)?对于长度为 1 的区间 [i, i],四个量的值都和 ai 相等。对于长度大于 1 的区间:
- 首先最好维护的是
iSum
,区间 [l, r] 的iSum
就等于「左子区间」的iSum
加上「右子区间」的iSum
。 - 对于 [l, r] 的
lSum
,存在两种可能,它要么等于「左子区间」的lSum
,要么等于「左子区间」的iSum
加上「右子区间」的lSum
,二者取大。 - 对于 [l, r] 的
rSum
,同理,它要么等于「右子区间」的rSum
,要么等于「右子区间」的iSum
加上「左子区间」的rSum
,二者取大。 - 当计算好上面的三个量之后,就很好计算 [l, r] 的
mSum
了。我们可以考虑 [l, r] 的mSum
对应的区间是否跨越 m——它可能不跨越 m,也就是说 [l, r] 的mSum
可能是「左子区间」的mSum
和 「右子区间」的mSum
中的一个;它也可能跨越 m,可能是「左子区间」的rSum
和 「右子区间」的lSum
求和。三者取大。
这样问题就得到了解决。
1 | class Solution { |
1 | Success: |
#完整代码
1 | package xyz.onns.leetcode; |