1982. Find Array Given Subset Sums
Problem Description
You need to recover an unknown array of length n
by using information about all its subset sums.
Given:
- An integer
n
representing the length of the unknown array you want to find - An array
sums
containing all2^n
possible subset sums of the unknown array (these sums are given in no particular order)
Your task is to determine what the original array was and return it. If there are multiple valid arrays that could produce the given subset sums, you can return any one of them.
Key concepts to understand:
- A subset of an array is formed by selecting zero or more elements from the original array
- A subset sum is the sum of all elements in a subset
- The empty subset has a sum of
0
- For an array of length
n
, there are exactly2^n
possible subsets (including the empty subset)
For example, if the unknown array was [1, 2]
:
- The subsets would be:
[]
,[1]
,[2]
,[1, 2]
- The subset sums would be:
0
,1
,2
,3
The problem guarantees that there will always be at least one valid solution for the given test cases.
The challenge lies in working backwards from the subset sums to figure out what elements must have been in the original array. This requires carefully analyzing the relationships between different subset sums to deduce the individual elements.
Intuition
The key insight is that we can iteratively extract elements from the subset sums by exploiting a fundamental property: when we know an element x
is in the original array, we can partition all subset sums into two groups - those that include x
and those that don't.
Let's think about this step by step:
-
Handling negative numbers: Since the array can contain negative numbers, we first shift all subset sums by adding the minimum value's absolute value (
m = -min(sums)
). This ensures all working values are non-negative, making them easier to handle. -
Finding the first element: After shifting and removing
0
(empty subset), the smallest remaining value must be one of the original array elements. Why? Because the smallest non-zero subset sum can only come from a single element (the smallest positive element after shifting). -
Finding subsequent elements: Once we know the first
i
elements, we have2^i
subset sums formed by these elements. We can calculate and remove all these known subset sums from our list. After removal, the smallest remaining value must be the next element of the original array. -
The iterative process:
- Start with element at position 0:
ans[0] = smallest non-zero value
- For position
i
, remove all subset sums formed by combinations ofans[0]
throughans[i-1]
- The smallest remaining value becomes
ans[i]
- This works because any remaining subset sum must include at least one element we haven't discovered yet, and the smallest such sum uses only one new element
- Start with element at position 0:
-
Restoring original signs: After finding all elements in their shifted form, we need to determine which elements were originally negative. We find a subset whose sum equals
m
(the shift amount). The elements in this subset were originally negative, so we flip their signs.
This greedy approach works because at each step, we're guaranteed that the smallest unexplained subset sum reveals a new element of the original array.
Learn more about Divide and Conquer patterns.
Solution Approach
Let's walk through the implementation step by step:
1. Initial Setup and Shifting:
m = -min(sums)
sl = SortedList(x + m for x in sums)
sl.remove(0)
- Calculate
m
as the absolute value of the minimum element insums
- Create a
SortedList
with all values shifted bym
to make them non-negative - Remove
0
(representing the empty subset) from the sorted list - Using
SortedList
allows efficient insertion and removal while maintaining sorted order
2. Finding the First Element:
ans = [sl[0]]
- After removing
0
, the smallest value in the sorted list must be an element from the original array - This becomes our first recovered element
3. Iteratively Finding Remaining Elements:
for i in range(1, n):
for j in range(1 << i):
if j >> (i - 1) & 1:
s = sum(ans[k] for k in range(i) if j >> k & 1)
sl.remove(s)
ans.append(sl[0])
- For each position
i
from 1 to n-1:- Generate all subsets of the first
i
elements using bit manipulation 1 << i
gives us2^i
possible subsets- The condition
j >> (i - 1) & 1
ensures we only consider subsets that include the most recently added elementans[i-1]
- For each such subset, calculate its sum and remove it from the sorted list
- After removing all known subset sums, the smallest remaining value is the next element
- Generate all subsets of the first
4. Restoring Original Signs:
for i in range(1 << n):
s = sum(ans[j] for j in range(n) if i >> j & 1)
if s == m:
for j in range(n):
if i >> j & 1:
ans[j] *= -1
break
- Iterate through all
2^n
possible subsets of the recovered array - Find the subset whose sum equals
m
(the shift amount) - The elements in this subset were originally negative before shifting
- Multiply these elements by -1 to restore their original negative values
- Once found, break out of the loop
Key Techniques Used:
- Bit manipulation for subset generation:
i >> j & 1
checks if the j-th bit of i is set - SortedList data structure for maintaining sorted order with efficient removals
- Greedy selection: Always choosing the smallest unexplained value as the next element
- Shift-and-restore pattern: Working with non-negative values then restoring signs at the end
The algorithm has a time complexity of O(2^n * n)
for generating and processing all subsets, which is acceptable given that the input already contains 2^n
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 work through a concrete example where the original array is [1, -3, 2]
.
Step 1: Understanding the input
- Original array:
[1, -3, 2]
(this is what we need to recover) - All possible subsets and their sums:
[]
β 0[1]
β 1[-3]
β -3[2]
β 2[1, -3]
β -2[1, 2]
β 3[-3, 2]
β -1[1, -3, 2]
β 0
- Given
sums = [0, 1, -3, 2, -2, 3, -1, 0]
(in no particular order)
Step 2: Shift all values to make them non-negative
- Find minimum:
min(sums) = -3
- Shift amount:
m = -(-3) = 3
- Shifted sums:
[3, 4, 0, 5, 1, 6, 2, 3]
- Create SortedList:
[0, 1, 2, 3, 3, 4, 5, 6]
- Remove 0 (empty subset):
[1, 2, 3, 3, 4, 5, 6]
Step 3: Find first element
- Smallest value in list:
1
- First element:
ans[0] = 1
- Current array:
[1]
Step 4: Find second element
- Remove subsets formed by
[1]
:- Subset
[1]
has sum1
β remove from list
- Subset
- Updated list:
[2, 3, 3, 4, 5, 6]
- Smallest value:
2
- Second element:
ans[1] = 2
- Current array:
[1, 2]
Step 5: Find third element
- Remove subsets formed by
[1, 2]
that includeans[1] = 2
:- Subset
[2]
has sum2
β remove - Subset
[1, 2]
has sum3
β remove
- Subset
- Updated list:
[3, 4, 5, 6]
- Smallest value:
3
- Third element:
ans[2] = 3
- Current array:
[1, 2, 3]
Step 6: Restore original signs
- We need to find which subset sums to
m = 3
- Check all subsets of
[1, 2, 3]
:[]
β 0 (not 3)[1]
β 1 (not 3)[2]
β 2 (not 3)[3]
β 3 β (equals 3!)
- Elements in this subset were originally negative
- Flip sign of element at index 2:
ans[2] = -3
Final Result: [1, 2, -3]
Note that the order might differ from the original [1, -3, 2]
, but both arrays produce the same set of subset sums, which is what matters for this problem.
Solution Implementation
1class Solution:
2 def recoverArray(self, n: int, sums: List[int]) -> List[int]:
3 # Find the sum of all negative elements (minimum subset sum)
4 negative_sum_offset = -min(sums)
5
6 # Create sorted list with all sums shifted by the offset to make them non-negative
7 # This ensures the empty subset sum becomes 0
8 sorted_sums = SortedList(subset_sum + negative_sum_offset for subset_sum in sums)
9
10 # Remove the empty subset sum (which is 0 after shifting)
11 sorted_sums.remove(0)
12
13 # The smallest remaining sum must be one of the original array elements
14 result = [sorted_sums[0]]
15
16 # Find remaining n-1 elements
17 for element_index in range(1, n):
18 # Remove all subset sums that include the elements found so far
19 for subset_mask in range(1 << element_index):
20 # Check if this subset includes the most recently found element
21 if subset_mask >> (element_index - 1) & 1:
22 # Calculate sum of this subset
23 subset_sum = sum(result[bit_pos] for bit_pos in range(element_index)
24 if subset_mask >> bit_pos & 1)
25 sorted_sums.remove(subset_sum)
26
27 # After removal, the smallest sum is the next element
28 result.append(sorted_sums[0])
29
30 # Determine which elements should be negative
31 # Try all possible sign combinations
32 for sign_mask in range(1 << n):
33 # Calculate sum with current sign assignment
34 current_sum = sum(result[bit_pos] for bit_pos in range(n)
35 if sign_mask >> bit_pos & 1)
36
37 # If this combination gives us the original negative sum
38 if current_sum == negative_sum_offset:
39 # Apply negative signs to selected elements
40 for bit_pos in range(n):
41 if sign_mask >> bit_pos & 1:
42 result[bit_pos] *= -1
43 break
44
45 return result
46
1class Solution {
2 public int[] recoverArray(int n, int[] sums) {
3 // Find the minimum value in sums array to determine the offset
4 // This offset ensures all values become non-negative
5 int minValue = Integer.MAX_VALUE;
6 for (int sum : sums) {
7 minValue = Math.min(minValue, sum);
8 }
9 int offset = -minValue;
10
11 // Create a TreeMap to store frequency of each sum (after applying offset)
12 // TreeMap keeps elements sorted, which helps us get the smallest element easily
13 TreeMap<Integer, Integer> sumFrequencyMap = new TreeMap<>();
14 for (int sum : sums) {
15 sumFrequencyMap.merge(sum + offset, 1, Integer::sum);
16 }
17
18 // Initialize result array to store the recovered elements
19 int[] result = new int[n];
20
21 // Remove one occurrence of 0 (empty subset sum after offset)
22 if (sumFrequencyMap.merge(0, -1, Integer::sum) == 0) {
23 sumFrequencyMap.remove(0);
24 }
25
26 // The first element is the smallest remaining sum
27 result[0] = sumFrequencyMap.firstKey();
28
29 // Iteratively find remaining elements
30 for (int i = 1; i < n; ++i) {
31 // Generate all subsets of first i elements
32 for (int subset = 0; subset < (1 << i); ++subset) {
33 // Only process subsets that include the (i-1)th element
34 if ((subset >> (i - 1) & 1) == 1) {
35 // Calculate sum of current subset
36 int subsetSum = 0;
37 for (int k = 0; k < i; ++k) {
38 if (((subset >> k) & 1) == 1) {
39 subsetSum += result[k];
40 }
41 }
42 // Remove this subset sum from the frequency map
43 if (sumFrequencyMap.merge(subsetSum, -1, Integer::sum) == 0) {
44 sumFrequencyMap.remove(subsetSum);
45 }
46 }
47 }
48 // The next element is the smallest remaining sum
49 result[i] = sumFrequencyMap.firstKey();
50 }
51
52 // Adjust signs: find which elements should be negative
53 // Check all possible sign combinations to find one that sums to the offset
54 for (int signMask = 0; signMask < (1 << n); ++signMask) {
55 int currentSum = 0;
56 // Calculate sum with current sign assignment
57 for (int j = 0; j < n; ++j) {
58 if (((signMask >> j) & 1) == 1) {
59 currentSum += result[j];
60 }
61 }
62 // If this combination gives us the offset, apply negative signs
63 if (currentSum == offset) {
64 for (int j = 0; j < n; ++j) {
65 if (((signMask >> j) & 1) == 1) {
66 result[j] *= -1;
67 }
68 }
69 break;
70 }
71 }
72
73 return result;
74 }
75}
76
1class Solution {
2public:
3 vector<int> recoverArray(int n, vector<int>& sums) {
4 // Find the minimum sum (most negative) and compute offset to make all sums non-negative
5 int minSum = *min_element(sums.begin(), sums.end());
6 int offset = -minSum;
7
8 // Create a multiset of all sums shifted by the offset (making them non-negative)
9 multiset<int> remainingSums;
10 for (int sum : sums) {
11 remainingSums.insert(sum + offset);
12 }
13
14 // Remove 0 (empty subset sum after offset)
15 remainingSums.erase(remainingSums.begin());
16
17 // Array to store the recovered positive values
18 vector<int> result;
19
20 // First element is the smallest remaining sum
21 result.push_back(*remainingSums.begin());
22
23 // Recover remaining n-1 elements
24 for (int currentIndex = 1; currentIndex < n; ++currentIndex) {
25 // Remove all subset sums that include the newly added element
26 for (int subsetMask = 0; subsetMask < (1 << currentIndex); ++subsetMask) {
27 // Check if the previous element (at index currentIndex-1) is included
28 if (subsetMask >> (currentIndex - 1) & 1) {
29 int subsetSum = 0;
30
31 // Calculate sum of current subset
32 for (int elementIdx = 0; elementIdx < currentIndex; ++elementIdx) {
33 if (subsetMask >> elementIdx & 1) {
34 subsetSum += result[elementIdx];
35 }
36 }
37
38 // Remove this subset sum from remaining sums
39 remainingSums.erase(remainingSums.find(subsetSum));
40 }
41 }
42
43 // The smallest remaining sum is the next element
44 result.push_back(*remainingSums.begin());
45 }
46
47 // Find which elements need to be negated to achieve the original minimum sum
48 for (int subsetMask = 0; subsetMask < (1 << n); ++subsetMask) {
49 int subsetSum = 0;
50
51 // Calculate sum for current subset
52 for (int elementIdx = 0; elementIdx < n; ++elementIdx) {
53 if (subsetMask >> elementIdx & 1) {
54 subsetSum += result[elementIdx];
55 }
56 }
57
58 // If this subset sum equals the offset, negate these elements
59 if (subsetSum == offset) {
60 for (int elementIdx = 0; elementIdx < n; ++elementIdx) {
61 if (subsetMask >> elementIdx & 1) {
62 result[elementIdx] = -result[elementIdx];
63 }
64 }
65 break;
66 }
67 }
68
69 return result;
70 }
71};
72
1function recoverArray(n: number, sums: number[]): number[] {
2 // Find the minimum sum (most negative) and compute offset to make all sums non-negative
3 const minSum = Math.min(...sums);
4 const offset = -minSum;
5
6 // Create a multiset of all sums shifted by the offset (making them non-negative)
7 // Using Map to simulate multiset (value -> count)
8 const remainingSums = new Map<number, number>();
9 for (const sum of sums) {
10 const shiftedSum = sum + offset;
11 remainingSums.set(shiftedSum, (remainingSums.get(shiftedSum) || 0) + 1);
12 }
13
14 // Helper function to remove one occurrence of a value from the multiset
15 const removeFromMultiset = (value: number): void => {
16 const count = remainingSums.get(value) || 0;
17 if (count > 1) {
18 remainingSums.set(value, count - 1);
19 } else {
20 remainingSums.delete(value);
21 }
22 };
23
24 // Helper function to get the minimum value from the multiset
25 const getMinFromMultiset = (): number => {
26 return Math.min(...remainingSums.keys());
27 };
28
29 // Remove 0 (empty subset sum after offset)
30 removeFromMultiset(0);
31
32 // Array to store the recovered positive values
33 const result: number[] = [];
34
35 // First element is the smallest remaining sum
36 result.push(getMinFromMultiset());
37
38 // Recover remaining n-1 elements
39 for (let currentIndex = 1; currentIndex < n; currentIndex++) {
40 // Remove all subset sums that include the newly added element
41 for (let subsetMask = 0; subsetMask < (1 << currentIndex); subsetMask++) {
42 // Check if the previous element (at index currentIndex-1) is included
43 if ((subsetMask >> (currentIndex - 1)) & 1) {
44 let subsetSum = 0;
45
46 // Calculate sum of current subset
47 for (let elementIdx = 0; elementIdx < currentIndex; elementIdx++) {
48 if ((subsetMask >> elementIdx) & 1) {
49 subsetSum += result[elementIdx];
50 }
51 }
52
53 // Remove this subset sum from remaining sums
54 removeFromMultiset(subsetSum);
55 }
56 }
57
58 // The smallest remaining sum is the next element
59 result.push(getMinFromMultiset());
60 }
61
62 // Find which elements need to be negated to achieve the original minimum sum
63 for (let subsetMask = 0; subsetMask < (1 << n); subsetMask++) {
64 let subsetSum = 0;
65
66 // Calculate sum for current subset
67 for (let elementIdx = 0; elementIdx < n; elementIdx++) {
68 if ((subsetMask >> elementIdx) & 1) {
69 subsetSum += result[elementIdx];
70 }
71 }
72
73 // If this subset sum equals the offset, negate these elements
74 if (subsetSum === offset) {
75 for (let elementIdx = 0; elementIdx < n; elementIdx++) {
76 if ((subsetMask >> elementIdx) & 1) {
77 result[elementIdx] = -result[elementIdx];
78 }
79 }
80 break;
81 }
82 }
83
84 return result;
85}
86
Time and Space Complexity
Time Complexity: O(n * 2^n)
The time complexity analysis breaks down as follows:
- The outer loop runs
n
times (from creating the first element to the nth element) - For each iteration
i
, the inner loop runs2^i
times to remove subset sums from the sorted list - The removal operation from SortedList takes
O(log(2^n))
=O(n)
time - Total removal operations:
β(i=1 to n-1) 2^i = 2^n - 2
- Cost of all removals:
O((2^n - 2) * n)
=O(n * 2^n)
- The final loop to find the correct sign assignment runs through all
2^n
subsets once, with each subset sum calculation takingO(n)
time - Total time:
O(n * 2^n) + O(n * 2^n)
=O(n * 2^n)
Space Complexity: O(2^n)
The space complexity analysis:
- The SortedList
sl
stores all2^n
subset sums initially:O(2^n)
- The answer array
ans
storesn
elements:O(n)
- The bit manipulation operations use constant extra space:
O(1)
- Total space:
O(2^n) + O(n)
=O(2^n)
(since2^n
dominatesn
)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Duplicate Values in Subset Sums
The Pitfall: When removing subset sums from the sorted list, if there are duplicate values, you might accidentally remove the wrong occurrence. For example, if multiple different subsets produce the same sum, the algorithm needs to handle these duplicates correctly.
Why It Happens:
- The input array might contain duplicate elements or elements that combine to produce identical sums
- When we call
sorted_sums.remove(subset_sum)
, it only removes one occurrence - If the same sum appears multiple times (from different subsets), we need to ensure we're removing the correct number of occurrences
Solution: The current implementation actually handles this correctly by:
- Processing subsets in a specific order (only those containing the most recent element)
- Removing each sum exactly once per iteration
- The SortedList maintains all duplicates and removes them one at a time
However, if you were to modify the algorithm, ensure you track which sums have been accounted for.
2. Bit Manipulation Off-by-One Errors
The Pitfall: Getting the bit positions wrong when checking subset membership:
# Incorrect: might check wrong bit position if subset_mask >> element_index & 1: # Wrong! # Correct: check the (element_index - 1)th bit if subset_mask >> (element_index - 1) & 1: # Right!
Why It Happens:
- Array indices are 0-based, but we're iterating element_index from 1 to n-1
- The bit position for checking if element at index i-1 is included should be (i-1)
Solution: Always be careful with index calculations in bit manipulation:
- Use parentheses to ensure correct order of operations
- Remember that bit position k corresponds to array index k
- Test with small examples to verify bit patterns
3. Not Handling the Sign Restoration Correctly
The Pitfall: After finding the absolute values of array elements, incorrectly determining which elements should be negative:
# Wrong approach: assuming any subset that sums to offset works
for sign_mask in range(1 << n):
current_sum = sum(result[j] for j in range(n) if sign_mask >> j & 1)
if current_sum == negative_sum_offset:
# Forgetting to actually apply the negative signs!
return result # Wrong - returns all positive values
Why It Happens:
- The algorithm shifts all values to be non-negative for processing
- After finding the elements, we need to restore the original signs
- Simply finding which subset sums to the offset isn't enough - we must apply the sign changes
Solution: Always complete the sign restoration:
if current_sum == negative_sum_offset:
for bit_pos in range(n):
if sign_mask >> bit_pos & 1:
result[bit_pos] *= -1 # Actually apply the negative sign
break
4. Inefficient Data Structure Choice
The Pitfall: Using a regular list instead of SortedList and manually sorting after each operation:
# Inefficient approach
sums_list = list(sums)
sums_list.sort()
# ... later in the code
sums_list.remove(subset_sum)
sums_list.sort() # O(n log n) each time!
Why It Happens:
- Not being aware of the SortedList data structure from the sortedcontainers library
- Trying to use built-in Python lists which don't maintain sorted order after modifications
Solution: Use SortedList which maintains sorted order automatically:
- O(log n) insertion and removal
- Always maintains sorted order without explicit sorting
- Provides efficient access to elements by index
5. Edge Case: Array Contains Zero
The Pitfall: If the original array contains 0, the algorithm might get confused since removing subsets with 0 doesn't change the sum:
# If original array is [0, 1, 2], subset sums include: # [], [0], [1], [2], [0,1], [0,2], [1,2], [0,1,2] # Sums: 0, 0, 1, 2, 1, 2, 3, 3 (duplicates!)
Why It Happens:
- Zero doesn't contribute to subset sums
- This creates duplicate values in the sums array
- The algorithm needs to handle these duplicates correctly
Solution: The current algorithm handles this correctly because:
- SortedList maintains all duplicates
- We remove sums one at a time in a controlled manner
- The greedy selection of the smallest remaining value still works
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!