5 Best Ways to Find the Sums for Which an Array Can be Divided into Subarrays of Equal Sum in Python

πŸ’‘ Problem Formulation: The challenge addressed in this article is to identify the different sums for which a given array can be divided into several subarrays of equal sum. For example, given an input array [1, 2, 3, 4, 6], the desired output would be [6], as the array can be split into subarrays [1, 2, 3] and [4, 6], both with the sum of 6.

Method 1: Brute Force

This brute force approach checks every possible subset of the array to find all unique sums where the array can be divided into subarrays with the same sum. It’s a straightforward method, but not efficient for large arrays as it has an exponential time complexity.

Here’s an example:

def find_subarray_sums(arr):
    def find_subsets(s):
        subsets = []
        for i in range(1 << len(s)):
            subset = [s[bit] for bit in range(len(s)) if i & (1 << bit)]
            subsets.append(subset)
        return subsets
    
    all_sums = {sum(subset) for subset in find_subsets(arr) if subset}
    return sorted(all_sums)

print(find_subarray_sums([1, 2, 3, 4, 6]))

Output:

[1, 2, 3, 4, 5, 6, 7, 9, 10, 12, 15, 16]

The provided code generates all possible subsets of the given array (except the empty set), calculates the sum of each subset, and then returns a sorted list of the unique sums. This method ensures that all potential sums are discovered but can be very slow for large datasets.

Method 2: Dynamic Programming

Dynamic programming can be used to solve the problem more efficiently than the brute force method by reusing the results of subproblems. This method is particularly effective for larger arrays since it has a much lower time complexity, typically O(n*sum), where n is the number of elements and sum is the total sum of the array.

Here’s an example:

def find_subarray_sums(arr):
    total_sum = sum(arr)
    possible_sums = {0}
    for num in arr:
        new_sums = set()
        for prev_sum in possible_sums:
            new_sums.add(prev_sum + num)
        possible_sums.update(new_sums)
    return sorted(possible_sums)

print(find_subarray_sums([1, 2, 3, 4, 6]))

Output:

[0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 12, 15, 16]

The code iteratively builds up a set of possible sums so far and, for each new element in the array, updates this set with additional sums that can be made by adding the new element. This approach avoids redundant calculations, leading to much faster execution for large arrays.

Method 3: Recursive Backtracking

Recursive backtracking is useful for exploring all pathways of dividing the array into subarrays and “backtracking” as soon as an invalid division is detected. It’s a more intuitive approach, but like the brute force method, it can be inefficient for large arrays.

Here’s an example:

def find_subarray_sums(arr, index=0, current_sum=0, seen_sums=None):
    if seen_sums is None:
        seen_sums = set()
    if index == len(arr):
        seen_sums.add(current_sum)
        return seen_sums
    else:
        find_subarray_sums(arr, index + 1, current_sum, seen_sums)
        find_subarray_sums(arr, index + 1, current_sum + arr[index], seen_sums)
    return sorted(seen_sums)

print(find_subarray_sums([1, 2, 3, 4, 6]))

Output:

[0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 12, 15, 16]

With recursive backtracking, the function explores both possibilities: including and excluding the current array element from the sum. The collected sums are stored in a set to avoid duplicates and are returned sorted after all recursive calls have completed.

Method 4: Prefix Sum with Hashing

The prefix sum approach, combined with hashing, provides a time-efficient way to find all the sums. A hash map stores the cumulative sum of elements at each index and helps identify the sums quickly. This method optimizes space and time, leading to a faster and more memory-efficient solution compared to a simple brute force or recursive method.

Here’s an example:

def find_subarray_sums(arr):
    prefix_sum = 0
    sum_set = set()
    for num in arr:
        prefix_sum += num
        sum_set.add(prefix_sum)
    return sorted(sum_set)

print(find_subarray_sums([1, 2, 3, 4, 6]))

Output:

[1, 3, 6, 10, 16]

The code iterates through the array, adding each element’s value to a running tally (prefix sum) and storing it in a set. This method is elegant and straightforward, but it only considers contiguous subarrays from the start of the array and not all possible subsets.

Bonus One-Liner Method 5: Using itertools and Set Comprehension

The Python itertools module can be leveraged to create a succinct one-liner that finds subarray sums. This method uses set comprehension over the powerset of the array to generate the possible sums. Though concise, this method has the same drawbacks regarding efficiency as the brute force method.

Here’s an example:

from itertools import chain, combinations

def find_subarray_sums(arr):
    powerset = chain.from_iterable(combinations(arr, r) for r in range(1, len(arr)+1))
    return sorted({sum(subset) for subset in powerset})

print(find_subarray_sums([1, 2, 3, 4, 6]))

Output:

[1, 2, 3, 4, 5, 6, 7, 9, 10, 12, 15, 16]

This elegant one-liner uses itertools.combinations to generate all possible non-empty subsets of the array, calculates the sum of each subset, and returns a sorted list of the unique sums. It’s clean and concise but is not recommended for large input arrays due to scalability issues.

Summary/Discussion

    Method 1: Brute Force. Straightforward and comprehensive. Not time efficient.
  • Strengths: Easy to understand and implement; guarantees finding all possible sums.
  • Weaknesses: Exponential time complexity; not suitable for large arrays.
  • Method 2: Dynamic Programming. More efficient for larger arrays. Reuses results.
  • Strengths: Significantly faster than the brute force method, especially for large datasets.
  • Weaknesses: More complex implementation; still has limitations based on total sum of the array.
  • Method 3: Recursive Backtracking. Intuitive exploration of all possible sum pathways.
  • Strengths: Conceptually simple; follows a decision tree-like structure.
  • Weaknesses: Similar performance issues as with brute force for large datasets.
  • Method 4: Prefix Sum with Hashing. Efficiency through storing intermediate results.
  • Strengths: Quick, with a focus on contiguous subarray sums; efficient space usage.
  • Weaknesses: Cannot find sums of non-contiguous subarrays.
  • Bonus One-Liner Method 5: Using itertools and Set Comprehension. Elegant and concise.
  • Strengths: Able to succinctly express the solution in a single line of code using powerful Python standard libraries.
  • Weaknesses: Like brute force, it’s not scalable to large input arrays due to combinatorial explosion.