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.
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 ini
, 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 selectn - i
from the second half - We need to find pairs
(a, b)
wherea ∈ f[i]
andb ∈ 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 subsetsO(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 EvaluatorExample 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):
Bitmask | Selected for A | Selected for B | Contribution | Count |
---|---|---|---|---|
00 | {} | {1, 2} | 0 - 1 - 2 = -3 | 0 |
01 | {1} | {2} | 1 - 2 = -1 | 1 |
10 | {2} | {1} | 2 - 1 = 1 | 1 |
11 | {1, 2} | {} | 1 + 2 = 3 | 2 |
So f[0] = {-3}
, f[1] = {-1, 1}
, f[2] = {3}
Step 3: Generate all contributions for the second half [3, 4]
Bitmask | Selected for A | Selected for B | Contribution | Count |
---|---|---|---|---|
00 | {} | {3, 4} | 0 - 3 - 4 = -7 | 0 |
01 | {3} | {4} | 3 - 4 = -1 | 1 |
10 | {4} | {3} | 4 - 3 = 1 | 1 |
11 | {3, 4} | {} | 3 + 4 = 7 | 2 |
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}
andg[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
- From
-
Case i = 1: Pick 1 from first half, 1 from second half
- From
f[1] = {-1, 1}
andg[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
- From
-
Case i = 2: Pick 2 from first half, 0 from second half
- From
f[2] = {3}
andg[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
- From
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 performsn
operations to calculate sums and counts. This givesO(2^n * n)
. - The second part iterates through
n+1
possible values ofi
. For eachi
:- Sorting
f[i]
andg[n-i]
takesO(2^n * log(2^n))
in the worst case (when all elements are in one set) - For each element in
fi
(up to2^n
elements), binary search is performed ongi
which takesO(log(2^n))
time - This gives
O(2^n * log(2^n))
for each value ofi
- Sorting
- 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 sincen
is typically small compared to2^n
, this can be simplified toO(n * 2^n)
Space Complexity: O(2^n)
The space analysis:
- The dictionaries
f
andg
each store at mostn+1
keys - For each key, the set can contain up to
C(n, k)
elements wherek
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
andgi
use additional space but are bounded byO(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
In a binary min heap, the maximum element can be found in:
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!