Facebook Pixel

2035. Partition Array Into Two Arrays to Minimize Sum Difference

Problem Description

You have an integer array nums containing exactly 2 * n integers. Your task is to split this array into two separate arrays, each containing exactly n integers. The goal is to minimize the absolute difference between the sums of these two arrays.

Here's what you need to do:

  • Take the original array of 2 * n integers
  • Divide all elements into two groups of size n each
  • Each element must go into exactly one of the two groups
  • Calculate the sum of each group
  • Find the arrangement that gives the smallest possible absolute difference between these two sums

For example, if nums = [3, 9, 7, 3] where n = 2, you could partition it as [3, 9] and [7, 3]. The sums would be 12 and 10, giving an absolute difference of |12 - 10| = 2.

The function should return the minimum possible absolute difference you can achieve through any valid partitioning.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that with 2n elements, trying all possible ways to partition them into two groups of size n would require checking C(2n, n) combinations, which becomes computationally infeasible for larger values of n.

To make this tractable, we can split the problem in half. Instead of looking at the entire array at once, we divide nums into two halves - the first n elements and the last n elements. Now we can think about the problem differently: from the first half, we'll pick some elements for group A and the rest go to group B. Similarly, from the second half, we'll pick some elements for group A and the rest go to group B.

The crucial observation is that if we pick i elements from the first half for group A, then we must pick exactly n - i elements from the second half for group A (to ensure group A has exactly n elements total). This constraint significantly reduces our search space.

For each half of the array, we can precompute all possible "contributions" to the difference between the two groups. If an element goes to group A, it contributes positively to the difference; if it goes to group B, it contributes negatively. By storing these contributions grouped by how many elements we're selecting, we can efficiently combine results from both halves.

The meet-in-the-middle technique allows us to reduce the time complexity from O(2^(2n)) to O(2^n * n). For each possible contribution from the first half (with i elements selected), we look for the best matching contribution from the second half (with n - i elements selected) that minimizes the total absolute difference. Binary search helps us quickly find the optimal pairing.

This approach transforms an intractable problem into a manageable one by cleverly decomposing it and using precomputation to enable efficient combination of partial results.

Learn more about Two Pointers, Binary Search, Dynamic Programming and Bitmask patterns.

Solution Approach

The implementation uses a meet-in-the-middle strategy with the following steps:

Step 1: Split and Precompute

We divide the array into two halves of size n each. For each half, we generate all possible subset sums with their corresponding element counts:

n = len(nums) >> 1  # n = len(nums) // 2
f = defaultdict(set)  # stores contributions from first half
g = defaultdict(set)  # stores contributions from second half

Step 2: Generate All Possible Contributions

For each possible subset (represented by bitmask i from 0 to 2^n - 1):

  • We iterate through all n positions
  • If bit j is set in i, we add the element to our subset (contributes positively)
  • If bit j is not set, we subtract the element (contributes negatively)
for i in range(1 << n):  # iterate through all 2^n subsets
    s = cnt = 0          # s: contribution sum, cnt: number of selected elements
    s1 = cnt1 = 0        # for second half
    for j in range(n):
        if (i & (1 << j)) != 0:
            s += nums[j]         # add to group A
            cnt += 1
            s1 += nums[n + j]
            cnt1 += 1
        else:
            s -= nums[j]         # add to group B (negative contribution)
            s1 -= nums[n + j]

The dictionaries f[cnt] and g[cnt] store all possible contribution values when selecting exactly cnt elements from their respective halves.

Step 3: Combine Results Using Binary Search

For each possible split configuration:

  • If we select i elements from the first half, we must select n - i from the second half
  • We need to find pairs (a, b) where a ∈ f[i] and b ∈ g[n-i] that minimize |a + b|
for i in range(n + 1):
    fi, gi = sorted(list(f[i])), sorted(list(g[n - i]))
    for a in fi:
        b = -a  # ideal target value to make sum zero
        # [Binary search](/problems/binary-search-speedrun) for value closest to b in gi
        left, right = 0, len(gi) - 1
        while left < right:
            mid = (left + right) >> 1
            if gi[mid] >= b:
                right = mid
            else:
                left = mid + 1

For each value a from the first half, we use binary search to find the value in the second half closest to -a (which would give us a sum close to zero). We check both gi[left] and gi[left-1] to ensure we find the minimum absolute difference.

Time Complexity: O(2^n * n * log n) where:

  • O(2^n * n) for generating all subsets
  • O(2^n * log n) for the binary search operations

Space Complexity: O(2^n) to store all possible subset sums for both halves.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with nums = [1, 2, 3, 4] where n = 2.

Step 1: Split the array

  • First half: [1, 2]
  • Second half: [3, 4]

Step 2: Generate all contributions for the first half [1, 2]

For each subset, we calculate the contribution (elements in group A are positive, group B are negative):

BitmaskSelected for ASelected for BContributionCount
00{}{1, 2}0 - 1 - 2 = -30
01{1}{2}1 - 2 = -11
10{2}{1}2 - 1 = 11
11{1, 2}{}1 + 2 = 32

So f[0] = {-3}, f[1] = {-1, 1}, f[2] = {3}

Step 3: Generate all contributions for the second half [3, 4]

BitmaskSelected for ASelected for BContributionCount
00{}{3, 4}0 - 3 - 4 = -70
01{3}{4}3 - 4 = -11
10{4}{3}4 - 3 = 11
11{3, 4}{}3 + 4 = 72

So g[0] = {-7}, g[1] = {-1, 1}, g[2] = {7}

Step 4: Combine results

We need exactly n = 2 elements in group A. So if we pick i from first half, we need 2 - i from second half:

  • Case i = 0: Pick 0 from first half, 2 from second half

    • From f[0] = {-3} and g[2] = {7}
    • Combination: -3 + 7 = 4
    • This represents: Group A = {3, 4}, Group B = {1, 2}
    • Sums: A = 7, B = 3, difference = |7 - 3| = 4
  • Case i = 1: Pick 1 from first half, 1 from second half

    • From f[1] = {-1, 1} and g[1] = {-1, 1}
    • For a = -1: Find closest to -(-1) = 1 in g[1]. We have 1 exactly!
      • Combination: -1 + 1 = 0
      • This represents: Group A = {1, 4}, Group B = {2, 3}
      • Sums: A = 5, B = 5, difference = |5 - 5| = 0
    • For a = 1: Find closest to -1 in g[1]. We have -1 exactly!
      • Combination: 1 + (-1) = 0
      • This represents: Group A = {2, 3}, Group B = {1, 4}
      • Sums: A = 5, B = 5, difference = |5 - 5| = 0
  • Case i = 2: Pick 2 from first half, 0 from second half

    • From f[2] = {3} and g[0] = {-7}
    • Combination: 3 + (-7) = -4
    • This represents: Group A = {1, 2}, Group B = {3, 4}
    • Sums: A = 3, B = 7, difference = |3 - 7| = 4

Result: The minimum absolute difference is 0, achieved by partitioning into {1, 4} and {2, 3} (or equivalently {2, 3} and {1, 4}).

Solution Implementation

1class Solution:
2    def minimumDifference(self, nums: List[int]) -> int:
3        # Split the array into two halves
4        half_size = len(nums) // 2
5      
6        # Dictionary to store possible differences for each subset size in left half
7        left_half_differences = defaultdict(set)
8        # Dictionary to store possible differences for each subset size in right half
9        right_half_differences = defaultdict(set)
10      
11        # Generate all possible subset combinations for the left half
12        for mask in range(1 << half_size):
13            difference = 0
14            subset_size = 0
15          
16            # Calculate difference (sum of selected - sum of unselected) for left half
17            for bit_position in range(half_size):
18                if (mask & (1 << bit_position)) != 0:
19                    # Element is selected for first subset
20                    difference += nums[bit_position]
21                    subset_size += 1
22                else:
23                    # Element is selected for second subset
24                    difference -= nums[bit_position]
25          
26            left_half_differences[subset_size].add(difference)
27      
28        # Generate all possible subset combinations for the right half
29        for mask in range(1 << half_size):
30            difference = 0
31            subset_size = 0
32          
33            # Calculate difference (sum of selected - sum of unselected) for right half
34            for bit_position in range(half_size):
35                if (mask & (1 << bit_position)) != 0:
36                    # Element is selected for first subset
37                    difference += nums[half_size + bit_position]
38                    subset_size += 1
39                else:
40                    # Element is selected for second subset
41                    difference -= nums[half_size + bit_position]
42          
43            right_half_differences[subset_size].add(difference)
44      
45        minimum_difference = float('inf')
46      
47        # Try all possible combinations where left contributes i elements to first subset
48        # and right contributes (n-i) elements to first subset
49        for left_subset_size in range(half_size + 1):
50            right_subset_size = half_size - left_subset_size
51          
52            # Convert sets to sorted lists for binary search
53            left_differences_sorted = sorted(list(left_half_differences[left_subset_size]))
54            right_differences_sorted = sorted(list(right_half_differences[right_subset_size]))
55          
56            # For each difference in left half, find the closest matching difference in right half
57            for left_diff in left_differences_sorted:
58                # We want to find right_diff closest to -left_diff
59                # so that left_diff + right_diff is minimized
60                target = -left_diff
61              
62                # Binary search for the closest value to target
63                left_index = 0
64                right_index = len(right_differences_sorted) - 1
65              
66                while left_index < right_index:
67                    mid_index = (left_index + right_index) // 2
68                    if right_differences_sorted[mid_index] >= target:
69                        right_index = mid_index
70                    else:
71                        left_index = mid_index + 1
72              
73                # Check the found position and its neighbor for minimum difference
74                minimum_difference = min(minimum_difference, 
75                                       abs(left_diff + right_differences_sorted[left_index]))
76              
77                if left_index > 0:
78                    minimum_difference = min(minimum_difference, 
79                                           abs(left_diff + right_differences_sorted[left_index - 1]))
80      
81        return minimum_difference
82
1class Solution {
2    public int minimumDifference(int[] nums) {
3        // Get half the length of the array (array has 2n elements)
4        int halfLength = nums.length >> 1;
5      
6        // Map to store possible sum differences for each count of selected elements from left half
7        Map<Integer, Set<Integer>> leftHalfSums = new HashMap<>();
8        // Map to store possible sum differences for each count of selected elements from right half
9        Map<Integer, Set<Integer>> rightHalfSums = new HashMap<>();
10      
11        // Generate all possible subset selections using bitmask (2^n possibilities)
12        for (int mask = 0; mask < (1 << halfLength); ++mask) {
13            int leftSum = 0;
14            int leftSelectedCount = 0;
15            int rightSum = 0;
16            int rightSelectedCount = 0;
17          
18            // Check each bit position to determine if element is selected
19            for (int bitPosition = 0; bitPosition < halfLength; ++bitPosition) {
20                if ((mask & (1 << bitPosition)) != 0) {
21                    // Element is selected for subset A
22                    leftSum += nums[bitPosition];
23                    ++leftSelectedCount;
24                    rightSum += nums[halfLength + bitPosition];
25                    ++rightSelectedCount;
26                } else {
27                    // Element is selected for subset B (represented as negative)
28                    leftSum -= nums[bitPosition];
29                    rightSum -= nums[halfLength + bitPosition];
30                }
31            }
32          
33            // Store the sum difference for each count of selected elements
34            leftHalfSums.computeIfAbsent(leftSelectedCount, k -> new HashSet<>()).add(leftSum);
35            rightHalfSums.computeIfAbsent(rightSelectedCount, k -> new HashSet<>()).add(rightSum);
36        }
37      
38        int minDifference = Integer.MAX_VALUE;
39      
40        // Try all combinations where i elements from left half and (n-i) from right half go to subset A
41        for (int leftCount = 0; leftCount <= halfLength; ++leftCount) {
42            // Convert sets to sorted lists for binary search
43            List<Integer> leftSumsList = new ArrayList<>(leftHalfSums.get(leftCount));
44            List<Integer> rightSumsList = new ArrayList<>(rightHalfSums.get(halfLength - leftCount));
45            Collections.sort(leftSumsList);
46            Collections.sort(rightSumsList);
47          
48            // For each possible sum from left half, find the best matching sum from right half
49            for (int leftValue : leftSumsList) {
50                // We want to find a value in rightSumsList closest to -leftValue
51                // This minimizes |leftValue + rightValue|
52                int target = -leftValue;
53              
54                // Binary search to find the smallest value >= target
55                int left = 0;
56                int right = rightSumsList.size() - 1;
57                while (left < right) {
58                    int mid = (left + right) >> 1;
59                    if (rightSumsList.get(mid) >= target) {
60                        right = mid;
61                    } else {
62                        left = mid + 1;
63                    }
64                }
65              
66                // Check the found value and its predecessor for minimum difference
67                minDifference = Math.min(minDifference, Math.abs(leftValue + rightSumsList.get(left)));
68                if (left > 0) {
69                    minDifference = Math.min(minDifference, Math.abs(leftValue + rightSumsList.get(left - 1)));
70                }
71            }
72        }
73      
74        return minDifference;
75    }
76}
77
1class Solution {
2public:
3    int minimumDifference(vector<int>& nums) {
4        // Split array into two halves of size n each
5        int n = nums.size() >> 1;
6      
7        // Store subset sums for left and right halves
8        // Index i stores all possible sums when selecting exactly i elements
9        vector<vector<int>> leftHalfSums(n + 1), rightHalfSums(n + 1);
10      
11        // Generate all possible subset sums for both halves
12        // Using bitmask to represent all 2^n subsets
13        for (int mask = 0; mask < (1 << n); ++mask) {
14            int leftSum = 0, leftCount = 0;
15            int rightSum = 0, rightCount = 0;
16          
17            // Process each bit position to determine subset membership
18            for (int bit = 0; bit < n; ++bit) {
19                if (mask & (1 << bit)) {
20                    // Element is selected for subset A
21                    leftSum += nums[bit];
22                    ++leftCount;
23                    rightSum += nums[n + bit];
24                    ++rightCount;
25                } else {
26                    // Element is selected for subset B (represented as negative)
27                    leftSum -= nums[bit];
28                    rightSum -= nums[n + bit];
29                }
30            }
31          
32            // Store the difference (sumA - sumB) for each subset size
33            leftHalfSums[leftCount].push_back(leftSum);
34            rightHalfSums[rightCount].push_back(rightSum);
35        }
36      
37        // Sort all subset sums for binary search
38        for (int i = 0; i <= n; ++i) {
39            sort(leftHalfSums[i].begin(), leftHalfSums[i].end());
40            sort(rightHalfSums[i].begin(), rightHalfSums[i].end());
41        }
42      
43        int minDifference = INT_MAX;
44      
45        // For each possible split, find the best matching pair
46        for (int leftElements = 0; leftElements <= n; ++leftElements) {
47            // Need to select (n - leftElements) from right half for subset A
48            int rightElements = n - leftElements;
49          
50            for (int leftDiff : leftHalfSums[leftElements]) {
51                // Find value in right half that minimizes |leftDiff + rightDiff|
52                // Target value is -leftDiff for sum to be zero
53                int target = -leftDiff;
54              
55                // Binary search for closest value to target
56                int left = 0, right = rightHalfSums[rightElements].size() - 1;
57                while (left < right) {
58                    int mid = (left + right) >> 1;
59                    if (rightHalfSums[rightElements][mid] >= target) {
60                        right = mid;
61                    } else {
62                        left = mid + 1;
63                    }
64                }
65              
66                // Check the found element and its predecessor for minimum difference
67                minDifference = min(minDifference, abs(leftDiff + rightHalfSums[rightElements][left]));
68                if (left > 0) {
69                    minDifference = min(minDifference, abs(leftDiff + rightHalfSums[rightElements][left - 1]));
70                }
71            }
72        }
73      
74        return minDifference;
75    }
76};
77
1function minimumDifference(nums: number[]): number {
2    // Split array into two halves of size n each
3    const n = nums.length >> 1;
4  
5    // Store subset sums for left and right halves
6    // Index i stores all possible sums when selecting exactly i elements
7    const leftHalfSums: number[][] = Array(n + 1).fill(null).map(() => []);
8    const rightHalfSums: number[][] = Array(n + 1).fill(null).map(() => []);
9  
10    // Generate all possible subset sums for both halves
11    // Using bitmask to represent all 2^n subsets
12    for (let mask = 0; mask < (1 << n); mask++) {
13        let leftSum = 0;
14        let leftCount = 0;
15        let rightSum = 0;
16        let rightCount = 0;
17      
18        // Process each bit position to determine subset membership
19        for (let bit = 0; bit < n; bit++) {
20            if (mask & (1 << bit)) {
21                // Element is selected for subset A
22                leftSum += nums[bit];
23                leftCount++;
24                rightSum += nums[n + bit];
25                rightCount++;
26            } else {
27                // Element is selected for subset B (represented as negative)
28                leftSum -= nums[bit];
29                rightSum -= nums[n + bit];
30            }
31        }
32      
33        // Store the difference (sumA - sumB) for each subset size
34        leftHalfSums[leftCount].push(leftSum);
35        rightHalfSums[rightCount].push(rightSum);
36    }
37  
38    // Sort all subset sums for binary search
39    for (let i = 0; i <= n; i++) {
40        leftHalfSums[i].sort((a, b) => a - b);
41        rightHalfSums[i].sort((a, b) => a - b);
42    }
43  
44    let minDifference = Number.MAX_SAFE_INTEGER;
45  
46    // For each possible split, find the best matching pair
47    for (let leftElements = 0; leftElements <= n; leftElements++) {
48        // Need to select (n - leftElements) from right half for subset A
49        const rightElements = n - leftElements;
50      
51        for (const leftDiff of leftHalfSums[leftElements]) {
52            // Find value in right half that minimizes |leftDiff + rightDiff|
53            // Target value is -leftDiff for sum to be zero
54            const target = -leftDiff;
55          
56            // Binary search for closest value to target
57            let left = 0;
58            let right = rightHalfSums[rightElements].length - 1;
59            while (left < right) {
60                const mid = (left + right) >> 1;
61                if (rightHalfSums[rightElements][mid] >= target) {
62                    right = mid;
63                } else {
64                    left = mid + 1;
65                }
66            }
67          
68            // Check the found element and its predecessor for minimum difference
69            minDifference = Math.min(minDifference, Math.abs(leftDiff + rightHalfSums[rightElements][left]));
70            if (left > 0) {
71                minDifference = Math.min(minDifference, Math.abs(leftDiff + rightHalfSums[rightElements][left - 1]));
72            }
73        }
74    }
75  
76    return minDifference;
77}
78

Time and Space Complexity

Time Complexity: O(2^n * n + n * 2^n * log(2^n)) which simplifies to O(n * 2^n)

The analysis breaks down as follows:

  • The first loop iterates through all 2^n possible subsets of the first half of the array. For each subset, it performs n operations to calculate sums and counts. This gives O(2^n * n).
  • The second part iterates through n+1 possible values of i. For each i:
    • Sorting f[i] and g[n-i] takes O(2^n * log(2^n)) in the worst case (when all elements are in one set)
    • For each element in fi (up to 2^n elements), binary search is performed on gi which takes O(log(2^n)) time
    • This gives O(2^n * log(2^n)) for each value of i
  • Total for the second part: O(n * 2^n * log(2^n)) = O(n * 2^n * n) = O(n^2 * 2^n)
  • The dominant term is O(n^2 * 2^n), but since n is typically small compared to 2^n, this can be simplified to O(n * 2^n)

Space Complexity: O(2^n)

The space analysis:

  • The dictionaries f and g each store at most n+1 keys
  • For each key, the set can contain up to C(n, k) elements where k is the key value
  • The total number of unique sums across all keys is bounded by 2^n (total number of subsets)
  • Each dictionary therefore uses O(2^n) space
  • The sorted lists fi and gi use additional space but are bounded by O(2^n)
  • Total space complexity: O(2^n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Interpretation of Subset Contribution

Pitfall: A common misunderstanding is thinking that the "difference" stored represents the sum of selected elements only. In reality, it represents sum(selected) - sum(unselected), which is the contribution to the overall difference between the two partition sums.

Why this matters: If you only store sums of selected elements without considering the unselected ones, you can't correctly calculate the final difference between partitions.

Solution: Always calculate the contribution as:

if element is selected for first subset:
    difference += element  # positive contribution
else:
    difference -= element  # negative contribution (goes to second subset)

2. Off-by-One Error in Binary Search

Pitfall: The binary search implementation can easily have off-by-one errors, especially when searching for the closest value rather than an exact match. A common mistake is:

# Incorrect - might miss the optimal value
while left <= right:  # Using <= instead of <
    mid = (left + right) // 2
    if right_differences_sorted[mid] >= target:
        right = mid - 1  # This could skip the optimal value
    else:
        left = mid + 1

Solution: Use the correct binary search pattern for finding the leftmost position where value >= target:

while left < right:
    mid = (left + right) // 2
    if right_differences_sorted[mid] >= target:
        right = mid  # Keep mid as a candidate
    else:
        left = mid + 1
# After loop, check both positions left and left-1

3. Missing Edge Cases in Binary Search Result

Pitfall: Only checking the position found by binary search without considering adjacent positions:

# Incorrect - only checks one position
minimum_difference = min(minimum_difference, 
                        abs(left_diff + right_differences_sorted[left_index]))

Solution: Always check both the found position and its neighbor:

# Check the found position
minimum_difference = min(minimum_difference, 
                        abs(left_diff + right_differences_sorted[left_index]))
# Also check the previous position if it exists
if left_index > 0:
    minimum_difference = min(minimum_difference, 
                            abs(left_diff + right_differences_sorted[left_index - 1]))

4. Incorrect Subset Size Pairing

Pitfall: Forgetting that when selecting i elements from the left half for the first subset, you must select exactly n - i elements from the right half for the first subset (not i elements):

# Incorrect pairing
for i in range(half_size + 1):
    left_diffs = left_half_differences[i]
    right_diffs = right_half_differences[i]  # Wrong! Should be half_size - i

Solution: Ensure complementary subset sizes:

for left_subset_size in range(half_size + 1):
    right_subset_size = half_size - left_subset_size  # Complementary size
    left_diffs = left_half_differences[left_subset_size]
    right_diffs = right_half_differences[right_subset_size]

5. Integer Overflow in Large Sums

Pitfall: For arrays with large values, the sum calculations might overflow in languages with fixed integer sizes. While Python handles arbitrary precision integers, this could be an issue when porting to other languages.

Solution: In Python, this isn't an issue, but when implementing in other languages, consider:

  • Using long/int64 data types
  • Normalizing values by subtracting the mean from all elements first
  • Working with differences directly rather than absolute sums
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More