“最大子数组和”的变型
思路和算法
如果我们用 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(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 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(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:
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
记 nums
元素个数为 n
。
nums
,故渐进时间复杂度为 O ( n ) O(n) O(n)。小记
“最大子数组和”是DP算法里的经典案例之一,经典到这个解法甚至有一个名称Kadane’s Algorithm。本题是“最大子数组和”的变型,但Kadane’s Algo依然适用。唯一要注意的是“乘法”下由于两个负数的乘积也依然可能得到一个很大的正数,所以必须同时计算“最小子数组和”,除此之外无任何区别。