【DP】乘积最大子数组
思路和算法
如果我们用 fmax(i)
来表示以第 i
个元素结尾的乘积最大子数组的乘积,a
表示输入参数 nums
,那么根据「53. 最大子序和」的经验,我们很容易推导出这样的状态转移方程:
fmax(i) = max{f(i-1)×a[i], a[i]}
它表示以第 i
个元素结尾的乘积最大子数组的乘积可以考虑 a[i]
加入前面的 fmax(i-1)
对应的一段,或者单独成为一段,这里两种情况下取最大值。求出所有的 fmax(i)
之后选取最大的一个作为答案。
分析错误与修正
这样做本来看似没有问题,但是如果 a = {5, 6, -3, 4, -3}
,按照前面的算法可以得到答案为 30,即前两个数的乘积。实际上,更大的乘积是包含全体数字的乘积。问题出在哪里呢?最后一个 -3
所对应的 fmax
的值既不是 -3
,也不是 4×(-3)
,而是 5×6×(-3)×4×(-3)
。这说明当前位置的最优解未必是由前一个位置的最优解转移得到的。
正负性的分类讨论
考虑当前位置如果是一个负数的话,我们希望以它前一个位置结尾的某个段的积也是个负数,这样就可以负负得正,并且我们希望这个积尽可能「负得更多」,即尽可能小。如果当前位置是一个正数的话,我们更希望以它前一个位置结尾的某个段的积也是个正数,并且希望它尽可能地大。因此,我们维护一个 fmin(i)
,它表示以第 i
个元素结尾的乘积最小子数组的乘积,从而得到以下的动态规划转移方程:
fmax(i) = max{fmax(i-1)×a[i], fmin(i-1)×a[i], a[i]}
fmin(i) = min{fmax(i-1)×a[i], fmin(i-1)×a[i], a[i]}
这样,第 i
个元素结尾的乘积最大或最小子数组的乘积可以通过加入第 i-1
个元素结尾的乘积最大或最小的子数组的乘积中,二者加上 a[i]
,三者取大或小,决定第 i
个元素结尾的乘积最大或最小子数组的乘积。
来源:力扣(LeetCode)
class Solution:
def maxProduct(self, nums):
if not nums:
return 0
maxF = nums[:]
minF = nums[:]
for i in range(1, len(nums)):
maxF[i] = max(maxF[i - 1] * nums[i], nums[i], minF[i - 1] * nums[i])
minF[i] = min(minF[i - 1] * nums[i], nums[i], maxF[i - 1] * nums[i])
return max(maxF)