Facebook Pixel

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 all 2^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 exactly 2^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.

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

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:

  1. 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.

  2. 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).

  3. Finding subsequent elements: Once we know the first i elements, we have 2^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.

  4. 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 of ans[0] through ans[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
  5. 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 in sums
  • Create a SortedList with all values shifted by m 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 us 2^i possible subsets
    • The condition j >> (i - 1) & 1 ensures we only consider subsets that include the most recently added element ans[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

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 Evaluator

Example 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 sum 1 β†’ remove from list
  • 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 include ans[1] = 2:
    • Subset [2] has sum 2 β†’ remove
    • Subset [1, 2] has sum 3 β†’ remove
  • 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 runs 2^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 taking O(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 all 2^n subset sums initially: O(2^n)
  • The answer array ans stores n elements: O(n)
  • The bit manipulation operations use constant extra space: O(1)
  • Total space: O(2^n) + O(n) = O(2^n) (since 2^n dominates n)

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:

  1. Processing subsets in a specific order (only those containing the most recent element)
  2. Removing each sum exactly once per iteration
  3. 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More