【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)
posted @ 2024-05-04 23:16  peterzh6  阅读(11)  评论(0编辑  收藏  举报