题目链接:https://leetcode.cn/problems/maximum-subarray/
1. 暴力法
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
length = len(nums)
max_sum = float('-inf')
for i in range(length):
sum_sub_array = 0
for j in range(i, length):
sum_sub_array += nums[j]
max_sum = max(max_sum, sum_sub_array)
return max_sum
2. 动态规划
关键 1:理解题意
题目要我们找出和最大的连续子数组的值是多少,「连续」是关键字,连续很重要,不是子序列。
题目只要求返回结果,不要求得到最大的连续子数组是哪一个。这样的问题通常可以使用「动态规划」解决。
关键 2:如何定义子问题(如何定义状态)
设计状态思路:把不确定的因素确定下来,进而把子问题定义清楚,把子问题定义得简单。动态规划的思想通过解决了一个一个简单的问题,进而把简单的问题的解组成了复杂的问题的解。
我们 不知道和最大的连续子数组一定会选哪一个数,那么我们可以求出 所有 经过输入数组的某一个数的连续子数组的最大和。
例如,示例 1 输入数组是 [-2,1,-3,4,-1,2,1,-5,4] ,我们可以求出以下子问题:
一共 9 个子问题,这些子问题之间的联系并没有那么好看出来,这是因为 子问题的描述还有不确定的地方(这件事情叫做「有后效性」,我们在本文的最后会讲解什么是「无后效性」)。
例如「子问题 3」:经过 −3 的连续子数组的最大和是多少。
「经过 −3 的连续子数组」我们任意举出几个:
我们不确定的是:−3 是连续子数组的第几个元素。那么我们就把 -3 定义成连续子数组的最后一个元素。在新的定义下,我们列出子问题如下:
我们加上了「结尾的」,这些子问题之间就有了联系。我们单独看子问题 1 和子问题 2:
大家发现了吗,如果编号为 i i i 的子问题的结果是负数或者 0 ,那么编号为 i + 1 i + 1 i+1 的子问题就可以把编号为 i i i 的子问题的结果舍弃掉(这里 i i i 为整数,最小值为 1 ,最大值为 8),这是因为:
因为我们把子问题定义的更清楚,子问题之间的联系就容易观察到。这是我们定义子问题、定义状态的经验。
接下来我们按照编写动态规划题解的步骤,把「状态定义」「状态转移方程」「初始化」「输出」「是否可以空间优化」全都写出来。
定义状态(定义子问题)
dp[i]:表示以 nums[i] 结尾 的 连续 子数组的最大和。
说明:「结尾」和「连续」是关键字。
状态转移方程(描述子问题之间的联系)
根据状态的定义,由于 nums[i] 一定会被选取,并且以 nums[i] 结尾的连续子数组与以 n u m s [ i − 1 ] nums[i-1] nums[i−1] 结尾的连续子数组只相差一个元素 nums[i] 。
假设数组 nums 的值全都严格大于 0,那么一定有 d p [ i ] = d p [ i − 1 ] + n u m s [ i ] dp[i] = dp[i - 1] + nums[i] dp[i]=dp[i−1]+nums[i]。
可是 d p [ i − 1 ] dp[i-1] dp[i−1] 有可能是负数,于是分类讨论:
以上两种情况的最大值就是 dp[i] 的值,写出如下状态转移方程:
d p [ i ] = { d p [ i − 1 ] + n u m s [ i ] , i f d p [ i − 1 ] > 0 n u m s [ i ] , i f d p [ i − 1 ] ≤ 0 dp[i] = \begin{cases} dp[i - 1] + nums[i], & if \quad dp[i - 1] > 0 \\ nums[i], & if \quad dp[i - 1] \le 0 \end{cases} dp[i]={dp[i−1]+nums[i],nums[i],ifdp[i−1]>0ifdp[i−1]≤0
记为「状态转移方程 1」。
状态转移方程还可以这样写,反正求的是最大值,也不用分类讨论了,就这两种情况,取最大即可,因此还可以写出状态转移方程如下:
d p [ i ] = max { n u m s [ i ] , d p [ i − 1 ] + n u m s [ i ] } dp[i] = \max \{nums[i],\; dp[i - 1] + nums[i]\} dp[i]=max{nums[i],dp[i−1]+nums[i]}
记为「状态转移方程 2」。
友情提示: 求解动态规划的问题经常要分类讨论,这是因为动态规划的问题本来就有「最优子结构」的特点,即大问题的最优解通常由小问题的最优解得到。因此我们在设计子问题的时候,就需要把求解出所有子问题的结果,进而选出原问题的最优解。
思考初始值
dp[0] 根据定义,只有 1 个数,一定以 nums[0] 结尾,因此 d p [ 0 ] = n u m s [ 0 ] dp[0] = nums[0] dp[0]=nums[0]。
思考输出
注意:
这里状态的定义不是题目中的问题的定义,不能直接将最后一个状态返回回去;这个问题的输出是把所有的 dp[0]、dp[1]、……、dp[n - 1]
都看一遍,取最大值。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
size = len(nums)
if size == 0:
return 0
dp = [0 for _ in range(size)]
dp[0] = nums[0]
for i in range(1, size):
if dp[i - 1] >= 0:
dp[i] = dp[i - 1] + nums[i]
else:
dp[i] = nums[i]
return max(dp)
进一步优化空间:
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
size = len(nums)
pre = 0
res = nums[0]
for i in range(size):
pre = max(nums[i], pre + nums[i])
res = max(res, pre)
return res
3. 贪心算法
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 时间复杂度:O(n), 遍历了一遍
# 空间复杂度:O(1), 用了2个变量
cur_sum=nums[0]
max_sum=nums[0]
# range范围是[1,len(nums)) 左开右闭,切记切记
for i in range(1,len(nums)):
# 若当前指针指向元素之前的和小于0,则丢弃此元素之前的数列(拖后腿的丢弃!!!)
# 当前和=当前值 与 当前值+之前最大和 的比较中较大的那个、
# 通俗易懂的理解:看当前这个值和之前数列的和,是否会拖当前这个值的后腿,如果扯后腿了说明没必要把之前的数列放到当前和,如果没有扯后腿则把最新的较大数放在当前和里面
cur_sum=max(nums[i],cur_sum+nums[i])
# 最大和=当前和 与 最大和 的比较中较大的那个
# 通俗易懂的理解:当前和就相当于当前潜在的最大和,把原来的最大和 与当前的潜在最大和进行比较,如果当前和比较大,则更换最大和,否则不更换
max_sum=max(cur_sum,max_sum)
return max_sum
4. 分治法
连续子序列的最大和主要由这三部分子区间里元素的最大和得到:
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
size = len(nums)
if size == 0:
return 0
return self.__max_sub_array(nums, 0, size - 1)
def __max_sub_array(self, nums, left, right):
if left == right:
return nums[left]
mid = (left + right) >> 1
return max(self.__max_sub_array(nums, left, mid),
self.__max_sub_array(nums, mid + 1, right),
self.__max_cross_array(nums, left, mid, right))
def __max_cross_array(self, nums, left, mid, right):
# 一定包含 nums[mid] 元素的最大连续子数组的和,
# 思路是看看左边"扩散到底",得到一个最大数,右边"扩散到底"得到一个最大数
# 然后再加上中间数
left_sum_max = 0
start_left = mid - 1
s1 = 0
while start_left >= left:
s1 += nums[start_left]
left_sum_max = max(left_sum_max, s1)
start_left -= 1
right_sum_max = 0
start_right = mid + 1
s2 = 0
while start_right <= right:
s2 += nums[start_right]
right_sum_max = max(right_sum_max, s2)
start_right += 1
return left_sum_max + nums[mid] + right_sum_max
题目链接:https://leetcode.cn/problems/maximum-product-subarray/description/
如果我们用 f max ( i ) f_{\max}(i) fmax(i) 来表示以第 i i i 个元素结尾的乘积最大子数组的乘积, a a a 表示输入参数 nums,那么根据前面「53. 最大子序和」的经验,我们很容易推导出这样的状态转移方程:
f max ( i ) = max i = 1 n { f ( i − 1 ) × a i , a i } f_{\max}(i) = \max_{i = 1}^{n} \{ f(i - 1) \times a_i, a_i \} fmax(i)=i=1maxn{f(i−1)×ai,ai}
它表示以第 i i i 个元素结尾的乘积最大子数组的乘积可以考虑 a i a_i ai 加入前面的 f max ( i − 1 ) f_{\max}(i - 1) fmax(i−1) 对应的一段,或者单独成为一段,这里两种情况下取最大值。求出所有的 f max ( i ) f_{\max}(i) fmax(i) 之后选取最大的一个作为答案。
可是在这里,这样做是错误的。为什么呢?
因为这里的定义并不满足「最优子结构」。具体地讲,如果 a = { 5 , 6 , − 3 , 4 , − 3 } a = \{ 5, 6, -3, 4, -3 \} a={5,6,−3,4,−3},那么此时 f max f_{\max} fmax 对应的序列是 { 5 , 30 , − 3 , 4 , − 3 } \{ 5, 30, -3, 4, -3 \} {5,30,−3,4,−3},按照前面的算法我们可以得到答案为 30,即前两个数的乘积,而实际上答案应该是全体数字的乘积。我们来想一想问题出在哪里呢?问题出在最后一个 −3 所对应的 f max f_{\max} fmax 的值既不是 −3,也不是 4 × ( − 3 ) 4 \times (-3) 4×(−3),而是 5 × 6 × ( − 3 ) × 4 × ( − 3 ) 5 \times 6 \times (-3) \times 4 \times (-3) 5×6×(−3)×4×(−3)。所以我们得到了一个结论:当前位置的最优解未必是由前一个位置的最优解转移得到的。
我们可以根据正负性进行分类讨论。
考虑当前位置如果是一个负数的话,那么我们希望以它前一个位置结尾的某个段的积也是个负数,这样就可以负负得正,并且我们希望这个积尽可能「负得更多」,即尽可能小。如果当前位置是一个正数的话,我们更希望以它前一个位置结尾的某个段的积也是个正数,并且希望它尽可能地大。于是这里我们可以再维护一个 f min ( i ) f_{\min}(i) fmin(i),它表示以第 i i i 个元素结尾的乘积最小子数组的乘积,那么我们可以得到这样的动态规划转移方程:
f max ( i ) = max i = 1 n { f max ( i − 1 ) × a i , f min ( i − 1 ) × a i , a i } f min ( i ) = min i = 1 n { f max ( i − 1 ) × a i , f min ( i − 1 ) × a i , a i } \begin{aligned} f_{\max}(i) &= \max_{i = 1}^{n} \{ f_{\max}(i - 1) \times a_i, f_{\min}(i - 1) \times a_i, a_i \} \\ f_{\min}(i) &= \min_{i = 1}^{n} \{ f_{\max}(i - 1) \times a_i, f_{\min}(i - 1) \times a_i, a_i \} \end{aligned} fmax(i)fmin(i)=i=1maxn{fmax(i−1)×ai,fmin(i−1)×ai,ai}=i=1minn{fmax(i−1)×ai,fmin(i−1)×ai,ai}
它代表第 i i i 个元素结尾的乘积最大子数组的乘积 f max ( i ) f_{\max}(i) fmax(i),可以考虑把 a i a_i ai 加入第 i − 1 i - 1 i−1 个元素结尾的乘积最大或最小的子数组的乘积中,二者加上 a i a_i ai, i i i 个元素结尾的乘积最大子数组的乘积。第 i i i 个元素结尾的乘积最小子数组的乘积 f min ( i ) f_{\min}(i) fmin(i) 同理。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
max_dp, min_dp = [nums[0]], [nums[0]]
for i in range(1, len(nums)):
max_dp.append(max(max_dp[i-1]*nums[i], min_dp[i-1]*nums[i], nums[i]))
min_dp.append(min(max_dp[i-1]*nums[i], min_dp[i-1]*nums[i], nums[i]))
return max(max_dp)
进一步优化:
class Solution:
def maxProduct(self, nums: List[int]) -> int:
'''动态规划'''
max_product, min_product, ans = nums[0], nums[0], nums[0]
for num in nums[1:]:
max_nums, min_nums = max_product, min_product
max_product = max(max_nums*num, min_nums*num, num)
min_product = min(max_nums*num, min_nums*num, num)
ans = max(ans, max_product)
return ans