题目:给定一个整数数组,找出总和最大的连续数列,并返回总和。
这个题目初看不会,看了解答还是不会,又看了解答终于想明白了,关键点就一个 连续数列
什么叫做连续数列,就是连在一起的数据。
例如有一个数列,我们要找其中连续的子序列,假设-1是我们最大连续子序列的最后一个数字,然后我们求出以这个数字为结尾的所有的连续的子序列,然后把每一个子序列求和
最后元素 | 后 | 面 | 不 | 管 | 子序列和 | |||||
---|---|---|---|---|---|---|---|---|---|---|
原始序列 | -2 | 1 | -3 | 4 | -1 | 2 | 1 | -5 | 4 | |
子序列1 | -1 | -1 | ||||||||
子序列2 | 4 | -1 | 3 | |||||||
子序列3 | -3 | 4 | -1 | 0 | ||||||
子序列4 | 1 | -3 | 4 | -1 | 1 | |||||
子序列5 | -2 | 1 | -3 | 4 | -1 | -1 |
然后我们再假设2是最大连续子序列的最后一个元素,再来求一遍每个子序列的和
最后元素 | 面 | 不 | 管 | 子序列和 | ||||||
---|---|---|---|---|---|---|---|---|---|---|
原始序列 | -2 | 1 | -3 | 4 | -1 | 2 | 1 | -5 | 4 | |
子序列1 | 2 | 2 | ||||||||
子序列1 | -1 | 2 | 1 | |||||||
子序列2 | 4 | -1 | 2 | 5 | ||||||
子序列3 | -3 | 4 | -1 | 2 | -2 | |||||
子序列4 | 1 | -3 | 4 | -1 | 2 | 3 | ||||
子序列5 | -2 | 1 | -3 | 4 | -1 | 2 | 1 |
有点感觉了吧,这其实就是把所有连续的子序列全部罗列一边,然后求出所有的连续的子序列的和。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
if nums == []:
return 0
dp = []
for end in range(1 ,len(nums)):
for start in reversed(range(end+1)):
dp.append(sum(nums[start:end])+nums[end])
return max(dp + nums)
同样的我们也可以以某个元素为开始计算连续子序列的和。
动态规划
紧接着就是我们的动态规划出场了,其实有点经验的人看到这个题目就知道使用动态规划,只是转移方程不会写。
加入我们已经知道以第i-1为结尾的最大子序列和
dp[i-1] | dp[i] | 子序列和 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
原始序列 | -2 | 1 | -3 | 4 | -1 | 2 | 1 | -5 | 4 | |
子序列1 | -2 | 1 | -3 | 4 | -1 | 3 | ||||
子序列2 | -2 | 1 | -3 | 4 | -1 | 2 | 5 |
因为是连续的(包含nums[i]以及前面的)所以直接可以知道dp[i] = dp[i-1]+nums[i],这里有一个很有趣的地方
dp[i-1] | nums[i] | 要不要加上 |
---|---|---|
正 | 正 | 都是正的加上肯定变大啊,跟前面的子序列连上变成更长的子序列 |
正 | 负 | 前面是正的,赶快加上前面的连续子序列,让自己变的大一点 |
负 | 负 | 本来就是负的了,再加上dp[i-1]的负数就更小了,不加,独立门户,自己就是连续子序列 |
负 | 正 | 自己是正的,为什么要跟前面的负数连在一起,独立门户,自己就是连续子序列 |
哈哈,咱们的转移方程就出现了,加上去比自己大,那就加上,如果加上去比自己还小了,肯定就不加了啊
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
if nums == []:
return 0
for i in range(1 ,len(nums)):
nums[i] = max(nums[i-1]+nums[i],nums[i])
return max(nums)
这个问题还可以更精简,如果前面是正的,咱们就加上,如果前面是负的,那么当前的最大子序列和就是自身nums[i]
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
if nums == []:
return 0
res,s = -float('inf'),0
for v in nums:
s += v
res = max(res,s)
if(s < 0): s = 0
return res
分治法
其实做这个题目我就是想要学习一下分治算法,没想到前面写了这么多。分治求解的思想非常的简单,我们的连续子序列有三种可能
- 和最大的子序列存在我们整个序列的左侧
- 和最大的子序列存在我们整个序列的左侧
- 横跨序列左半部分和右半部分:也就是说,中间这个数字是左边子序列的结尾,是右边子序列的开始,我们算一下以这个数字开始和结尾的左右子序列最大和,然后求和即可,说起来有点绕,看图
比较三者的大小,最大者即为所求的最大子序列和
左边 | 中间 | 右边 | 最大和 |
---|---|---|---|
最大子序列子这边 max_left | max_left | ||
以v结尾的和最大的子序列 max_v_left | v | 以v开始的和最大的子序列 max_v_right | max_v_left + v + max_v_right |
最大子序列子这边 max_right | max_right |
整体就是这个样子的,然后咱们写一个伪代码试试
def maxSubArray(nums):
l,r = 0,len(nums)
mid = (l+r) // 2
# 最大和子序列在左侧
max_left = maxSubArray(nums[:mid])
# 最大和子序列在右侧
max_right = maxSubArray(nums[mid:])
# # 最大和子序列两侧都包含元素
# 首先计算以mid为结尾的左侧最大子序列的和,这个最开始的那种方法一模一样啊
# 即,一个一个元素增加求每一个子序列的和
left_res,right_res = 0,0
left_max,rigth_max = -float('inf'),-float('inf')
# 以mid结束的前面所有连续子序列的和,求其中的最大(这里是去除了mid的值,要不然这里会被计算一次)
for v in reversed(nums[:mid]):
left_res += v
left_max = max(left_max,left_res)
# 以mid开始的前面所有连续子序列的和,求其中的最大 (然后这里还会被计算一次,所以上面去掉了mid)
for v in nums[mid:]:
right_res += v
rigth_max = max(rigth_max,right_res)
max_mid = left_max + rigth_max
return max(max_left,max_right,max_mid)