题目:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
除了常用的动态规划分治方法以外,还有一种基于贪心的算法,看起来很简单,细细想来实则很精妙:
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
if n < 1: return 0
res = float('-inf')
total = 0
for i in range(n):
if total < 0:
total = 0
total += nums[i]
res = max(res, total)
return res
简单来说,就是依次按数据中的元素求和,如果在某一个元素之前的累加和小于0,则和清零,从当前位开始继续向后累加,直到数据最后一个元素。就能找到有可能是最大子序和的子序和。
可以看到这个算法里的重点是在于判断累加和是否为零并清零的这一步,如果没有这一步,就变成了暴力求解,需要嵌套两个循环,复杂度就会是O(n2),太高了。
为什么只要和0比较就可以做到呢?为什么这个界限是0而不是1或者其它数呢?
答案是因为如果小于0,说明前面的整个子序是对找到最大子序和没有贡献的,不会使子序和增加,不需要被放到这个子序里。
那为什么可以直接抛弃呢,万一里面有一个相对大的正数可以对后面的子序有贡献呢?
我们可以归纳一下。
先从两个元素的开始,有两种情况:
- 负正:到第二次迭代这种情况实际上是不可能出现的,因为第一个负元素在第一次迭代时就已经被抛弃了。很容易理解,我们想找的最大子序的首尾肯定不可能是负的。
- 正负:如果这两个元素都被保留,说明这个正数的绝对值会大于等于这个负数的绝对值。
那再加入一个元素,也有两种情况:
- 正负负:如果这种情况累加和小于零,即使最前面元素的绝对值很大,它无法抵消后面负数的影响,这一串也要抛弃。
- 正负正:这种情况是一定可以保留的,因为新增的正数一定是会增加子序的和。
以此类推。
所以,即使里面有一个相对大的正数,它不可能在子串的末尾还被抛弃。它被抛弃的可能只有它(最前)+一串负数或相对小的正数+一个负数的情况,这种情况下,它没办法抵消这个子串中负数的影响,这个子串也对将来整体和的增大没有贡献,所以抛弃含有它的整个子串也无所谓。
这个问题能用贪心算法的关键有两个:
一是可以用数学归纳法(以上过程)来证明,有一个可行解(子串)之后,实现贪心选择(只添加有贡献的子串),最终可以达到找到最大子串(包含有贡献的子串)的目的。这可以满足贪心算法的最优子结构性质(一个问题的最优解包含其子问题的最优解)。
当然这个问题只需要求一个和,并不需要求最大子串的起点和终点,所以它用了一个res来记录所有可能的子串和。即使迭代到最后,后面的子串使累加和变小了也没关系,因为之前的累加和已经被res记录下来了。
这也是用来满足贪心算法所要求的第二个条件——贪心选择性质(每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择)。
用短短几行代码,只有一个循环一个判断就实现了满足这两个条件的贪心算法,不可谓之不妙。