LeetCode 152. 乘积最大子数组(Python、动态规划)

“最大子数组和”的变型

题目描述
LeetCode 152. 乘积最大子数组(Python、动态规划)_第1张图片

动态规划

思路和算法

如果我们用 f max ⁡ ( i ) f_{\max}(i) fmax(i) 来表示以第 i i i 个元素结尾的乘积最大子数组的乘积, a a a 表示输入参数 n u m s nums 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(i1)×ai,ai}

它表示以第 i i i 个元素结尾的乘积最大子数组的乘积可以考虑 a i a_i ai 加入前面的 f max ⁡ ( i − 1 ) f_{\max}(i - 1) fmax(i1) 对应的一段,或者单独成为一段,这里两种情况下取最大值。求出所有的 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 30 30,即前两个数的乘积,而实际上答案应该是全体数字的乘积。我们来想一想问题出在哪里呢?问题出在最后一个 − 3 -3 3 所对应的 f max ⁡ f_{\max} fmax 的值既不是 − 3 -3 3,也不是 4 × − 3 4 \times -3 4×3,而是 5 × 30 × ( − 3 ) × 4 × ( − 3 ) 5 \times 30 \times (-3) \times 4 \times (-3) 5×30×(3)×4×(3)。所以我们得到了一个结论:当前位置的最优解未必是由前一个位置的最优解转移得到的。

我们可以根据正负性进行分类讨论。

考虑当前位置如果是一个负数的话,那么我们希望以它前一个位置结尾的某个段的积也是个负数,这样就可以负负得正,并且我们希望这个积尽可能「负得更多」,即尽可能小。如果当前位置是一个正数的话,我们更希望以它前一个位置结尾的某个段的积也是个正数,并且希望它尽可能地大于是这里我们可以再维护一个 f min ⁡ ( i ) f_{\min}(i) fmin(i),它表示以第 ii 个元素结尾的乘积最小子数组的乘积,那么我们可以得到这样的动态规划转移方程:
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(i1)×ai,fmin(i1)×ai,ai}=i=1minn{fmax(i1)×ai,fmin(i1)×ai,ai}

它代表第 i i i 个元素结尾的乘积最大子数组的乘积 f max ⁡ ( i ) f_{\max}(i) fmax(i),可以考虑把 a i a_i ai 加入第 i − 1 i - 1 i1 个元素结尾的乘积最大或最小的子数组的乘积中,二者加上 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:
        min_dp = 1    # 记录最小乘积
        max_dp = 1    # 记录最大乘积
        maxProduct = nums[0]
        for i in range(len(nums)):
            min_temp = min_dp
            max_temp = max_dp
            min_dp = min(min_temp * nums[i], max_temp * nums[i], nums[i])
            max_dp = max(min_temp * nums[i], max_temp * nums[i], nums[i])
            maxProduct = max(maxProduct, max_dp)
        return maxProduct

LeetCode 152. 乘积最大子数组(Python、动态规划)_第2张图片
复杂度分析

nums 元素个数为 n

  • 时间复杂度:程序一次循环遍历了 nums,故渐进时间复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度:优化后只使用常数个临时变量作为辅助空间,与 n n n 无关,故渐进空间复杂度为 O ( 1 ) O(1) O(1)

小记

“最大子数组和”是DP算法里的经典案例之一,经典到这个解法甚至有一个名称Kadane’s Algorithm。本题是“最大子数组和”的变型,但Kadane’s Algo依然适用。唯一要注意的是“乘法”下由于两个负数的乘积也依然可能得到一个很大的正数,所以必须同时计算“最小子数组和”,除此之外无任何区别。

你可能感兴趣的:(LeetCode,动态规划,LeetCode,LeetCode,数组,leetcode,python,动态规划,算法,数组)