Given an integer array
nums, find a subarray that has the largest product, and return the product. The test cases are generated so that the answer will fit in a 32-bit integer.Note that the product of an array with a single element is the value of that element.
Constraints:
1 <= nums.length <= 2 * 104-10 <= nums[i] <= 10- The product of any subarray of
numsis guaranteed to fit in a 32-bit integer.
First, this is a dynamic programming problem: the product of the array is related to the product of its subarray , which is related to the product of the subarray , which suggests this is a problem with an optimal substructure. The fact that it is a subarray and therefore can exist anywhere inside as long as it is contiguous means that a DP approach seems like the best way forward.
This problem, however, is unique and therefore quite tricky: unlike most dynamic programming questions, the Bellman Equation is not a scalar field , but a vector field !
To begin, think about just two values and their product . Assume that is constant and is a variable.
How do we use this to solve the problem? The approach we can take is to iterate through the array from left to right, calculating the maximum subarray as we go according to the rules above. Our Bellman equation must follow this, and therefore it must track both the minimum and the maximum of the subarrays!
Let be the maximum and minimum products in the subarray . As usual for DP questions, we compare and , as well as the products between them. Looking at just for now:
The same goes for calculating but all the inequalities are flipped, and we take the minimum value. To sum up,
Note that the Bellman equation calculates subarrays that end at a specific index, and thus we can’t simply return ! In fact, we are calculating
We can do a bottom-up approach to calculate that.
def maxProduct(self, nums: List[int]) -> int:
# matrix storing all min and max of the product subarrays ending at index i
opts = [[nums[0], nums[0]]]
for i in range(1, len(nums)):
curr_min = opts[i - 1][0]
curr_max = opts[i - 1][1]
# Bellman equation
curr_opt = [
min(nums[i], nums[i] * curr_min, nums[i] * curr_max),
max(nums[i], nums[i] * curr_min, nums[i] * curr_max)
]
# update the matrix
opts.append(curr_opt)
return max(opt[1] for opt in opts)
We can simplify this further.
opts[i] once, to calculate opts[i + 1]. Thus, we can just use two variables curr_min, curr_max to do the calculations instead. Notice that they call each other in their calculations, so variable assignment must take place concurrently or temporary values must be used.def maxProduct(self, nums: List[int]) -> int:
largest = float('-inf')
curr_min, curr_max = nums[0], nums[0]
for i in range(1, len(nums)):
curr_min, curr_max = (
min(nums[i], nums[i] * curr_min, nums[i] * curr_max),
max(nums[i], nums[i] * curr_min, nums[i] * curr_max)
)
largest = max(curr_max, largest)
return largest
A top-down approach starts with the Bellman Equation, calculating
where , and
def maxProduct(self, nums: List[int]) -> int:
def maxSubProduct(j: int) -> (int, int):
if j == 0:
return nums[0], nums[0]
prev_min, prev_max = maxSubProduct(j - 1)
curr_min, curr_max = (
min(nums[i], nums[i] * prev_min, nums[i] * prev_max),
max(nums[i], nums[i] * prev_min, nums[i] * prev_max)
)
return curr_min, curr_max
# ...
This is where memoisation comes in. We only want to calculate maxSubProduct(i) once for each i, but maxSubProduct(i) calls maxSubProduct(i-1) every time. Therefore we memoise the calculated value to ensure there is no duplicate calculation.
def maxProduct(self, nums: List[int]) -> int:
memo = {0: (nums[0], nums[0])}
def maxSubProduct(i: int) -> (int, int):
if i in memo:
return memo[i]
prev_min, prev_max = maxSubProduct(i - 1)
curr_min, curr_max = (
min(nums[i], nums[i] * prev_min, nums[i] * prev_max),
max(nums[i], nums[i] * prev_min, nums[i] * prev_max)
)
memo[i] = (curr_min, curr_max)
return curr_min, curr_max
largest = float('-inf')
for i in range(0, len(nums)):
largest = max(largest, maxSubProduct(i)[1])
return largest
There is a for loop with constant time operation per iteration: the min and max comparisons are all fixed length, and the other operations are simply variable assignments or multiplication. This loops for iterations. The rest are constant time operations, so the algorithm runs in time.
There are only 3 variables used to calculate the product, so the algorithm has space complexity.