Update: OP has clarified that the entire array must be partitioned into complete subarrays. As such, the first half of this post constitutes a full answer, but the second half may still be useful as a soltuion to a generalization of this question.
If we're using the whole array, the problem can be solved in O(n) time, which is optimal. We get to use the fact that splitting A into k subarrays means all subarrays will have a sum of sum(A)/k. After computing the array sum, we can solve the problem in a single pass through the array, using just prefix sums and a hashmap.
If we're not using the whole array, the problem is harder. There's an O(n^2) solution where you compute all subarray sums, sort equal-sum subarrays by right endpoint, and use greedy interval scheduling. I have no idea if this is optimal, but I'm interested to see if there's a better solution.
Full array partition case
The precise problem definition here is to find the length of the longest sequence of points [x_0, x_1, ... x_m] such that 0 <= x_0 < x_1 < ... < x_m < length(A) such that sum(A[0:x_0]) == sum(A[x_0:x_1]) == ... == sum(A[x_m:length(A)]).
Suppose we've computed the sum of the array to be S. Then, we can split A into k parts of equal sum if and only if (k divides S and if we see S/k, 2S/k, ... , kS/k as a subsequence of the prefix sums of A). One easy way to do that is to keep a running sum: if our running sum r divides S, then save 2S/r in a hashmap as a 'sum we're searching for'. If our current sum is one we've been searching for, save the next number from that subsequence as a 'sum we're searching for', unless we've reached the end of that sequence.
For example, say that S is 32. Then, A can be partitioned into 8 equal sum subarrays if and only if 4, 8, 12, 16, 20, 24, 28 appears as a subsequence of the prefix sums of A (32 will always be present at the end). So as soon as we see 4 as a prefix sum, we check that it divides S, and then save 8 to our search set for prefix sums. We also keep a helper dictionary, mapping 8 to 4, so that after finding 8 as a prefix sum, we know that 8+4 is the next prefix sum to look for.
Python code:
def best_full_partition(nums: List[int]) -> int:
"""Given a list of integers (positive or negative) 'nums',
return the maximum number of disjoint equal sum subarrays we can
partition nums into (using all elements)"""
total = sum(nums)
# Special case where total sum is 0
if total == 0:
# Count the number of times 0 is a partial sum
answer = 0
current_sum = 0
for x in nums:
current_sum += x
if current_sum == 0:
answer += 1
return answer
best_found = 1
# Prefix sums we're trying to find; all share common factor with total
looking_for = set()
""" Map from prefix sums to the common factor/original prefix sum.
There may be several: e.g. total/6 and total/3 may be targets
of 2*total/3. """
looking_to_original_sums = collections.defaultdict(set)
current_sum = 0
for x in nums:
current_sum += x
if current_sum == 0:
continue
if current_sum in looking_for:
for original_sum in looking_to_original_sums[current_sum]:
new_target = current_sum + original_sum
# If we've found all matches in this chain
if new_target == total:
best_found = max(best_found, total // original_sum)
continue
looking_for.add(new_target)
looking_to_original_sums[new_target].add(original_sum)
looking_to_original_sums.pop(current_sum)
looking_for.discard(current_sum)
# Check if current sum is a divisor of full array sum
if total % current_sum == 0:
# If this splits array in half by sum, we've reached its end
if 2 * current_sum == total:
best_found = max(best_found, 2)
else:
# Add the next multiple of this sum to our search set
looking_for.add(2 * current_sum)
looking_to_original_sums[2 * current_sum].add(current_sum)
return best_found
This takes O(n) time, which is optimal, and O(n) space.
Partial array partition
This case is harder, because there's fewer conditions on what the valid subarray sums can be. The trick is to just compute the sum of all subarrays. We make a hashmap, mapping each sum to the index bounds of its subarray, so sum(A[L, L+1, ... R]) maps to [L, R]. Since there are duplicates, we keep a list of all intervals which produced that sum, and we generate that list to be sorted by the right endpoint.
Now, we can use earliest deadline first scheduling, aka greedy scheduling, to find the maximum number of intervals we can take from that list without overlap. Both steps take quadratic time. It may be possible to improve this, but I have no ideas for how to do so.
Python code:
def best_partial_partition(nums: List[int]) -> int:
"""Given a list of integers (positive or negative) 'nums',
return the maximum number of disjoint equal sum subarrays we can
create from nums (using all elements is not required)"""
n = len(nums)
best_found = 1
# For each subarray sum, stores a list of all subarrays with that sum
# Sorted by right endpoint, both ends inclusive
sum_to_intervals = collections.defaultdict(list)
for right_end in range(n):
curr_sum = 0
for left_end in reversed(range(right_end+1)):
curr_sum += nums[left_end]
sum_to_intervals[curr_sum].append([left_end, right_end])
# Use greedy interval scheduling to get most intervals
for interval_list in sum_to_intervals.values():
# Can skip if we know no improvement is possible here
if len(interval_list) <= best_found:
continue
curr_len = 0
curr_right_end = -1
for left, right in interval_list:
if left > curr_right_end:
curr_len += 1
curr_right_end = right
best_found = max(best_found, curr_len)
return best_found
This runs in O(n^2) time.
Edit: Fixed a bug in full partition solver when the array sum was 0; thanks to @josejuan for pointing this out. This case needs to be treated separately to avoid dividing by zero.