53. Maximum Subarray
Last Updated: 2020.06.05
Question Source: Maximum Subarray - LeetCode
Resources:
Runtime: O(N) because we traverse the array once only.
Space: O(1) because we only use a pointer and store values in variables.
This is the solution that I came up with by analyzing the properties of the question on my own. It's messy but helps lead to Kadane's Algorithm below.
Based on above insights, we can think of our subarray as starting from the left, and expanding toward the right. We must follow the following rules to get the largest possible value:
If the start is a negative number, then the subarray can only be of size one.
Move end pointer to the next element, do we include it in the subarray?
If element is negative:
But if the element is larger than the sum of the previous values in subarray, do NOT include it, and end or "close" the subarray there.
class Solution:
def maxSubArray(self, array):
"""
Finds the largest sum possible for a contingous subarray of input array
array type: list[]
rtype: int
"""
max_val = float('-inf')
cur_val = 0
for element in array:
# current element is neg
if element <= 0:
# previous subarray was negative, so start a new subarray by clearing cur_val to 0
if cur_val <= 0:
cur_val = 0
# since cur element is negative, we need to check if adding it to the subarray will result in a negative number. If yes, is the value value at least greater than our max_val?
# "element + cur_val <= 0" checks whether we are adding the negative number to subarray will keep subarray positivie.
# Ex 1: [8,-3] Yes - the net result is positive, so skip this and do NOT start a new subarray.
# Ex 2: [-3] No - the net result is negative. So we want to consider whether looking at this element by itself is still greater than the max_val. In this case -3 > -inf
if element + cur_val <= 0 and element + cur_val < max_val:
# In this case it is not, so start a new subarray by clearing cur_val
cur_val = 0
continue
else:
cur_val += element
max_val = max(cur_val,max_val)
# current element is positive
else:
# If the previous subarray was a single negative number, so start a new subarray by clearing cur_val to 0
if cur_val > 0:
cur_val += element
else:
cur_val = element
max_val = max(cur_val, max_val)
return max_val
s = Solution()
print(s.maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])) # 6
print(s.maxSubArray([-4])) # -4
print(s.maxSubArray([0])) # 0
print(s.maxSubArray([0,3])) # 3
print(s.maxSubArray([10,3])) # 13
print(s.maxSubArray([-10,-3,0,-5])) # -0
print(s.maxSubArray([-10,-3,-5])) # -3
print(s.maxSubArray([-2,1])) # 1
print(s.maxSubArray([])) # -inf
The previous solution is basically a messy implementation of Kadane's Algorithm.
The logic is:
i
[-2,1,-3,4,-1,2,1,-5,4] <- input array
[-2,1,-2,4, 3,5,6, 1,5] <- modified array
[-2,1, 1,4, 4,5,6, 6,6] <- max_sum
Runtime: O(N) since we have one pointer going through the length of the array
Space: O(1) since we are only using pointers
The leetcode solution:
class Solution:
def maxSubArray(self, nums: 'List[int]') -> 'int':
n = len(nums)
max_sum = nums[0]
for i in range(1, n):
if nums[i - 1] > 0:
nums[i] += nums[i - 1]
max_sum = max(nums[i], max_sum)
return max_sum
Another solution without mutating the given array:
class Solution:
def maxSubArray(self, nums):
global_max = float('-inf')
local_max = float('-inf')
for val in nums:
if local_max < 0:
local_max = val
else:
local_max += val
global_max = max(local_max, global_max)
return global_max
The logic is slightly different from Kadane's Algorithm:
cur_sum
)using a variablemax_val
) using a variable[-2,1,-3,4,-1,2,1,-5,4]
[-2,1,-2,4, 3,5,6, 1,5] <- cur_max
[-2,1, 1,4, 4,5,6, 6,6] <- max_val
class Solution:
def maxSubArray(self, nums):
max_val = nums[0]
cur_max = nums[0]
for i in range(1,len(nums)):
cur_max = max(nums[i], cur_max + nums[i])
max_val = max(cur_max, max_val)
return max_val
s = Solution()
print(s.maxSubArray([-2,1,-3,4,-1,2,1,-5,4]))
Looking at the trace for both Greedy & Kadane, they are exactly the same. Could Greedy and Kadane have different traces?
Actually no, they have the same traces, because the inherent properties of Math make the Greedy approach and Kadane's work in the same way.
Greedy
[-4,-9,-3,-2,0,1,3,-2,3]
[-4,-9,-3,-2,0,1,4, 2,5]
Kadane
[-4,-9,-3,-2,0,1,3,-2,3]
[-4,-9,-3,-2,0,1,4, 2,5]
Kadane says that a new subarray should be created if the previous one is negative.
Greedy says that we should add a number to our subarray only if the sum is greater than starting a new subarray.
In math, the only way that the sum of the current subarray plus a new item can be greater than taking the new item by itself is if the current subarray is positive.
Thus they end up behaving the same way and having the same trace.
Runtime: O(N*LogN)
Space: O(N*LogN)
This solution will actually give us the maximum subarray, unlike the DP solution which only gives us the sum of the max subarray.
The maximum subarray can either be:
The intuition is keep bi-secting the subarray into 2 halfs, cutting up and and comparing those 3 options
Starting
Level 1
Level 2
Level 2-Cross
In this example, for the right hand side subarray:
Level 0 - Cross
Level 0 - Final
class Solution:
def cross_sum(self, nums, left, right, p):
if left == right:
return nums[left]
left_subsum = float('-inf')
curr_sum = 0
for i in range(p, left - 1, -1):
curr_sum += nums[i]
left_subsum = max(left_subsum, curr_sum)
right_subsum = float('-inf')
curr_sum = 0
for i in range(p + 1, right + 1):
curr_sum += nums[i]
right_subsum = max(right_subsum, curr_sum)
return left_subsum + right_subsum
def helper(self, nums, left, right):
if left == right:
return nums[left]
p = (left + right) // 2
left_sum = self.helper(nums, left, p)
right_sum = self.helper(nums, p + 1, right)
cross_sum = self.cross_sum(nums, left, right, p)
return max(left_sum, right_sum, cross_sum)
def maxSubArray(self, nums: 'List[int]') -> 'int':
return self.helper(nums, 0, len(nums) - 1)