805. Split Array With Same Average
Problem Description
You are given an integer array nums
. Your task is to split all elements of nums
into two non-empty arrays A
and B
such that both arrays have the same average value.
The average of an array is calculated as the sum of all its elements divided by the number of elements in the array.
You need to return true
if it's possible to achieve such a split where average(A) == average(B)
, and false
otherwise.
Key Requirements:
- Every element from
nums
must be placed into either arrayA
or arrayB
- Both
A
andB
must be non-empty (contain at least one element) - The average values of
A
andB
must be exactly equal
Example scenarios:
- If
nums = [1,2,3,4,5,6,7,8]
, one possible split could beA = [1,4,5,8]
andB = [2,3,6,7]
, where both arrays have an average of 4.5 - If the array cannot be split to satisfy the conditions, return
false
The challenge lies in finding whether such a partition exists among all possible ways to divide the array elements.
Intuition
Let's think about what it means for two arrays to have the same average. If array A
has sum s1
and k
elements, and array B
has sum s2
and n-k
elements (where n
is the total number of elements), then:
average(A) = s1/k
and average(B) = s2/(n-k)
For these to be equal: s1/k = s2/(n-k)
Since s2 = s - s1
(where s
is the total sum), we can substitute:
s1/k = (s-s1)/(n-k)
Cross-multiplying and simplifying, we get:
s1 * (n-k) = (s-s1) * k
s1 * n = s * k
s1/k = s/n
This is a key insight! It means array A
's average must equal the overall array's average. In other words, we're looking for a subset whose average equals the total average.
Now, here's where it gets clever. Instead of dealing with averages (which might be fractions), we can transform the problem. If we subtract the overall average from each element, we're essentially asking: "Can we find a subset that sums to 0?"
But wait - the average s/n
might not be an integer, leading to floating-point issues. To avoid this, we multiply each element by n
first, giving us nums[i] * n
. Now our equation becomes (s1 * n)/k = s
, and after subtracting s
from each transformed element, we get nums[i] * n - s
.
With this transformation, finding a subset with the original average is equivalent to finding a subset that sums to 0 in our transformed array.
The final challenge is efficiency. With up to 30 elements, checking all 2^30
subsets would be too slow. This is where "meet in the middle" comes in - we split the array in half, enumerate all possible sums from each half (only 2^15
each), and check if sums from the left and right halves can combine to give 0. This reduces our time complexity from O(2^n)
to O(2^(n/2))
.
Learn more about Math, Dynamic Programming and Bitmask patterns.
Solution Approach
Let's implement the solution step by step based on our intuition:
Step 1: Handle Edge Case
if n == 1: return False
If there's only one element, we can't split it into two non-empty arrays.
Step 2: Transform the Array
s = sum(nums)
for i, v in enumerate(nums):
nums[i] = v * n - s
We transform each element by multiplying it by n
and subtracting the total sum s
. This converts our problem to finding a subset that sums to 0.
Step 3: Split the Array for Meet-in-the-Middle
m = n >> 1 # m = n // 2
We divide the array into two halves - the left half has m
elements, and the right half has n-m
elements.
Step 4: Enumerate All Subset Sums from the Left Half
vis = set()
for i in range(1, 1 << m):
t = sum(v for j, v in enumerate(nums[:m]) if i >> j & 1)
if t == 0:
return True
vis.add(t)
- We use bit manipulation to enumerate all non-empty subsets of the left half
i
ranges from1
to2^m - 1
, representing all possible subsets- For each bit position
j
ini
, if the bit is set (i >> j & 1
), we includenums[j]
in the subset - If we find a subset sum of 0, we immediately return
True
- Otherwise, we store all subset sums in the hash set
vis
Step 5: Enumerate All Subset Sums from the Right Half
for i in range(1, 1 << (n - m)):
t = sum(v for j, v in enumerate(nums[m:]) if i >> j & 1)
if t == 0 or (i != (1 << (n - m)) - 1 and -t in vis):
return True
- Similarly, we enumerate all non-empty subsets of the right half
- If we find a subset sum of 0, return
True
- Otherwise, check if
-t
exists invis
. If it does, it means we can combine a subset from the left (with sum-t
) and a subset from the right (with sumt
) to get a total sum of 0 - Important: The condition
i != (1 << (n - m)) - 1
ensures we don't select all elements from the right half. This is because if we select all elements from both halves, arrayB
would be empty, violating the problem constraints
Step 6: Return False if No Valid Split Found
return False
Time Complexity: O(2^(n/2) * n)
- We enumerate 2^(n/2)
subsets for each half, and calculating each subset sum takes O(n)
time.
Space Complexity: O(2^(n/2))
- The hash set vis
stores at most 2^(n/2)
subset sums.
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 a small example with nums = [1, 2, 3, 4]
.
Step 1: Initial Setup
- Total sum
s = 1 + 2 + 3 + 4 = 10
- Number of elements
n = 4
- Overall average =
10/4 = 2.5
Step 2: Transform the Array
We transform each element: nums[i] * n - s
1 * 4 - 10 = -6
2 * 4 - 10 = -2
3 * 4 - 10 = 2
4 * 4 - 10 = 6
Transformed array: [-6, -2, 2, 6]
Step 3: Split for Meet-in-the-Middle
- Left half:
[-6, -2]
(indices 0, 1) - Right half:
[2, 6]
(indices 2, 3)
Step 4: Enumerate Left Half Subsets Using bit manipulation (where bit position indicates element inclusion):
i = 1
(binary: 01): Include element 0 → subset[-6]
, sum =-6
i = 2
(binary: 10): Include element 1 → subset[-2]
, sum =-2
i = 3
(binary: 11): Include elements 0,1 → subset[-6, -2]
, sum =-8
Store in hash set: vis = {-6, -2, -8}
Step 5: Enumerate Right Half Subsets
i = 1
(binary: 01): Include element 0 → subset[2]
, sum =2
- Check if
-2
is invis
: YES! This means we found a match. - Left subset with sum
-2
is[-2]
(original element2
) - Right subset with sum
2
is[2]
(original element3
) - Combined sum =
-2 + 2 = 0
âś“
- Check if
Step 6: Verify the Original Split
- Array A: elements that contributed to sum 0 →
[2, 3]
- Array B: remaining elements →
[1, 4]
- Average of A:
(2 + 3) / 2 = 2.5
- Average of B:
(1 + 4) / 2 = 2.5
Both averages equal 2.5, so we return true
!
The key insight: By transforming the array, we converted the "equal average" problem into a "subset sum to 0" problem, which we efficiently solved using meet-in-the-middle to avoid checking all 2^n possible subsets.
Solution Implementation
1class Solution:
2 def splitArraySameAverage(self, nums: List[int]) -> bool:
3 # Get array length
4 array_length = len(nums)
5
6 # Edge case: cannot split array of size 1
7 if array_length == 1:
8 return False
9
10 # Transform the problem: if we can split into two sets A and B where
11 # sum(A)/len(A) = sum(B)/len(B) = total_sum/array_length
12 # This is equivalent to: sum(A) * array_length = len(A) * total_sum
13 # Rearranging: sum(A * array_length - total_sum) = 0 for each element in A
14 total_sum = sum(nums)
15 for index, value in enumerate(nums):
16 nums[index] = value * array_length - total_sum
17
18 # Split array into two halves for meet-in-the-middle approach
19 mid_point = array_length >> 1 # equivalent to array_length // 2
20
21 # Store all possible sums from the first half
22 first_half_sums = set()
23
24 # Generate all non-empty subsets of the first half using bit manipulation
25 for subset_mask in range(1, 1 << mid_point):
26 # Calculate sum of elements in current subset
27 subset_sum = sum(
28 value
29 for bit_position, value in enumerate(nums[:mid_point])
30 if subset_mask >> bit_position & 1
31 )
32
33 # If we found a subset with sum 0, we have a valid split
34 if subset_sum == 0:
35 return True
36
37 # Store this sum for later checking
38 first_half_sums.add(subset_sum)
39
40 # Generate all non-empty subsets of the second half
41 second_half_size = array_length - mid_point
42 for subset_mask in range(1, 1 << second_half_size):
43 # Calculate sum of elements in current subset
44 subset_sum = sum(
45 value
46 for bit_position, value in enumerate(nums[mid_point:])
47 if subset_mask >> bit_position & 1
48 )
49
50 # Check if this subset alone sums to 0
51 if subset_sum == 0:
52 return True
53
54 # Check if we can combine with a subset from first half to get sum 0
55 # But avoid the case where we select all elements (no valid split)
56 if subset_mask != (1 << second_half_size) - 1 and -subset_sum in first_half_sums:
57 return True
58
59 # No valid split found
60 return False
61
1class Solution {
2 public boolean splitArraySameAverage(int[] nums) {
3 int arrayLength = nums.length;
4
5 // Edge case: cannot split array of size 1
6 if (arrayLength == 1) {
7 return false;
8 }
9
10 // Calculate total sum of the array
11 int totalSum = Arrays.stream(nums).sum();
12
13 // Transform the problem: if we can split into two sets A and B where
14 // sum(A)/|A| = sum(B)/|B| = totalSum/arrayLength
15 // This is equivalent to: sum(A) * arrayLength = |A| * totalSum
16 // Rearranging: sum(A) * arrayLength - |A| * totalSum = 0
17 // For each element nums[i], its contribution becomes: nums[i] * arrayLength - totalSum
18 for (int i = 0; i < arrayLength; ++i) {
19 nums[i] = nums[i] * arrayLength - totalSum;
20 }
21
22 // Split array into two halves for meet-in-the-middle approach
23 int firstHalfSize = arrayLength >> 1; // Equivalent to arrayLength / 2
24 Set<Integer> firstHalfSums = new HashSet<>();
25
26 // Generate all possible subset sums for the first half
27 // Iterate through all non-empty subsets using bit manipulation
28 for (int mask = 1; mask < (1 << firstHalfSize); ++mask) {
29 int subsetSum = 0;
30
31 // Check each bit position to determine if element is in subset
32 for (int bitPosition = 0; bitPosition < firstHalfSize; ++bitPosition) {
33 if (((mask >> bitPosition) & 1) == 1) {
34 subsetSum += nums[bitPosition];
35 }
36 }
37
38 // If we found a subset with sum 0, we have a valid split
39 if (subsetSum == 0) {
40 return true;
41 }
42
43 // Store this sum for later checking with second half
44 firstHalfSums.add(subsetSum);
45 }
46
47 // Generate all possible subset sums for the second half
48 int secondHalfSize = arrayLength - firstHalfSize;
49 for (int mask = 1; mask < (1 << secondHalfSize); ++mask) {
50 int subsetSum = 0;
51
52 // Check each bit position to determine if element is in subset
53 for (int bitPosition = 0; bitPosition < secondHalfSize; ++bitPosition) {
54 if (((mask >> bitPosition) & 1) == 1) {
55 subsetSum += nums[firstHalfSize + bitPosition];
56 }
57 }
58
59 // Check if this subset sum is 0 (valid split found)
60 if (subsetSum == 0) {
61 return true;
62 }
63
64 // Check if we can combine with a subset from first half to get sum 0
65 // Important: avoid selecting all elements from second half when checking
66 // (mask != (1 << secondHalfSize) - 1) ensures we don't select all elements
67 if (mask != (1 << secondHalfSize) - 1 && firstHalfSums.contains(-subsetSum)) {
68 return true;
69 }
70 }
71
72 return false;
73 }
74}
75
1class Solution {
2public:
3 bool splitArraySameAverage(vector<int>& nums) {
4 int arraySize = nums.size();
5
6 // Cannot split an array of size 1
7 if (arraySize == 1) {
8 return false;
9 }
10
11 // Transform the problem: if we can split into two sets with same average,
12 // then sum1/size1 = sum2/size2 = totalSum/arraySize
13 // This means: sum1 * arraySize = totalSum * size1
14 // Transform each element: nums[i] = nums[i] * arraySize - totalSum
15 // Now we need to find a subset with sum = 0
16 int totalSum = accumulate(nums.begin(), nums.end(), 0);
17 for (int& value : nums) {
18 value = value * arraySize - totalSum;
19 }
20
21 // Meet in the middle approach: split array into two halves
22 int leftHalfSize = arraySize >> 1;
23 int rightHalfSize = arraySize - leftHalfSize;
24
25 // Store all possible sums from the left half (excluding empty set)
26 unordered_set<int> leftSums;
27 for (int mask = 1; mask < (1 << leftHalfSize); ++mask) {
28 int currentSum = 0;
29
30 // Calculate sum for current subset based on bitmask
31 for (int bitPos = 0; bitPos < leftHalfSize; ++bitPos) {
32 if (mask >> bitPos & 1) {
33 currentSum += nums[bitPos];
34 }
35 }
36
37 // If we found a subset with sum 0, we have a valid split
38 if (currentSum == 0) {
39 return true;
40 }
41
42 leftSums.insert(currentSum);
43 }
44
45 // Check all possible sums from the right half
46 for (int mask = 1; mask < (1 << rightHalfSize); ++mask) {
47 int currentSum = 0;
48
49 // Calculate sum for current subset based on bitmask
50 for (int bitPos = 0; bitPos < rightHalfSize; ++bitPos) {
51 if (mask >> bitPos & 1) {
52 currentSum += nums[leftHalfSize + bitPos];
53 }
54 }
55
56 // If right subset sum is 0, we have a valid split
57 if (currentSum == 0) {
58 return true;
59 }
60
61 // Check if we can combine with left subset to get sum 0
62 // Avoid using all elements from right half (mask != all bits set)
63 // because we need at least one element in the other partition
64 if (mask != (1 << rightHalfSize) - 1 && leftSums.count(-currentSum)) {
65 return true;
66 }
67 }
68
69 return false;
70 }
71};
72
1function splitArraySameAverage(nums: number[]): boolean {
2 const arraySize = nums.length;
3
4 // Cannot split an array of size 1
5 if (arraySize === 1) {
6 return false;
7 }
8
9 // Transform the problem: if we can split into two sets with same average,
10 // then sum1/size1 = sum2/size2 = totalSum/arraySize
11 // This means: sum1 * arraySize = totalSum * size1
12 // Transform each element: nums[i] = nums[i] * arraySize - totalSum
13 // Now we need to find a subset with sum = 0
14 const totalSum = nums.reduce((sum, num) => sum + num, 0);
15 for (let i = 0; i < nums.length; i++) {
16 nums[i] = nums[i] * arraySize - totalSum;
17 }
18
19 // Meet in the middle approach: split array into two halves
20 const leftHalfSize = arraySize >> 1;
21 const rightHalfSize = arraySize - leftHalfSize;
22
23 // Store all possible sums from the left half (excluding empty set)
24 const leftSums = new Set<number>();
25 for (let mask = 1; mask < (1 << leftHalfSize); mask++) {
26 let currentSum = 0;
27
28 // Calculate sum for current subset based on bitmask
29 for (let bitPos = 0; bitPos < leftHalfSize; bitPos++) {
30 if ((mask >> bitPos) & 1) {
31 currentSum += nums[bitPos];
32 }
33 }
34
35 // If we found a subset with sum 0, we have a valid split
36 if (currentSum === 0) {
37 return true;
38 }
39
40 leftSums.add(currentSum);
41 }
42
43 // Check all possible sums from the right half
44 for (let mask = 1; mask < (1 << rightHalfSize); mask++) {
45 let currentSum = 0;
46
47 // Calculate sum for current subset based on bitmask
48 for (let bitPos = 0; bitPos < rightHalfSize; bitPos++) {
49 if ((mask >> bitPos) & 1) {
50 currentSum += nums[leftHalfSize + bitPos];
51 }
52 }
53
54 // If right subset sum is 0, we have a valid split
55 if (currentSum === 0) {
56 return true;
57 }
58
59 // Check if we can combine with left subset to get sum 0
60 // Avoid using all elements from right half (mask !== all bits set)
61 // because we need at least one element in the other partition
62 if (mask !== (1 << rightHalfSize) - 1 && leftSums.has(-currentSum)) {
63 return true;
64 }
65 }
66
67 return false;
68}
69
Time and Space Complexity
Time Complexity: O(n Ă— 2^(n/2))
The algorithm uses a meet-in-the-middle approach by splitting the array into two halves. For the first half of size m = n/2
, it generates all possible subsets using bit manipulation from 1
to 2^m - 1
, which takes O(2^(n/2))
iterations. For each subset, it calculates the sum by iterating through at most m
elements, contributing O(n/2)
operations per subset. Similarly, for the second half of size n - m
, it generates all subsets from 1
to 2^(n-m) - 1
, which is also O(2^(n/2))
iterations, with each subset sum calculation taking O(n/2)
time. The initial transformation of the array takes O(n)
time. Therefore, the overall time complexity is O(n Ă— 2^(n/2))
.
Space Complexity: O(2^(n/2))
The algorithm stores all possible subset sums from the first half in a set vis
. Since there are at most 2^(n/2) - 1
non-empty subsets in the first half, the space required for the set is O(2^(n/2))
. The input array modification is done in-place, requiring no additional space. Thus, the total space complexity is O(2^(n/2))
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Check for Empty Array B
The Pitfall:
When using the meet-in-the-middle approach, a critical edge case occurs when we select all elements from both halves. This would mean array A contains all elements from nums
, leaving array B empty, which violates the constraint that both arrays must be non-empty.
Why It Happens:
In the second loop, when subset_mask = (1 << second_half_size) - 1
, all bits are set to 1, meaning we're selecting all elements from the second half. If we find a matching negative sum in first_half_sums
, we might incorrectly return True
even though this would result in selecting all elements for array A.
The Bug:
# Incorrect version - missing the check if -subset_sum in first_half_sums: # This could select all elements! return True
The Solution:
# Correct version - ensures we don't select all elements if subset_mask != (1 << second_half_size) - 1 and -subset_sum in first_half_sums: return True
2. Integer Overflow in Transformation
The Pitfall:
When transforming elements using value * array_length - total_sum
, the multiplication can cause integer overflow for large values, especially in languages with fixed integer sizes.
Example:
If nums = [100000, 100000, ...]
with many elements, value * array_length
could exceed integer limits.
The Solution: In Python, this isn't an issue due to arbitrary precision integers, but in other languages, consider:
- Using long/int64 data types
- Checking for potential overflow before multiplication
- Using alternative mathematical formulations if needed
3. Incorrect Subset Enumeration
The Pitfall: Using incorrect bit manipulation logic when enumerating subsets can lead to missing valid combinations or including invalid ones.
Common Mistakes:
# Wrong: Starting from 0 includes empty subset
for subset_mask in range(0, 1 << mid_point): # Includes empty set!
# Wrong: Incorrect bit checking
if subset_mask & (1 << bit_position): # Should be: subset_mask >> bit_position & 1
The Solution: Always start from 1 to exclude empty subsets and use proper bit shifting:
for subset_mask in range(1, 1 << mid_point): # Start from 1
if subset_mask >> bit_position & 1: # Correct bit checking
4. Missing Early Termination Opportunities
The Pitfall: Not checking for impossible cases early can lead to unnecessary computation.
Example:
If the total sum cannot be evenly distributed (considering the mathematical constraints), we should return False
immediately rather than performing expensive subset enumeration.
The Solution: Add mathematical feasibility checks before the main algorithm:
# Check if it's mathematically possible
# For a valid split with k elements in array A:
# sum(A)/k = total_sum/n
# This means sum(A) = k * total_sum / n must be an integer
for k in range(1, n):
if (k * total_sum) % n == 0:
# At least one valid k exists, proceed with algorithm
break
else:
return False # No valid k exists
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!