Find Array Given Subset Sums
HardPrompt
You are given an integer n representing the length of an unknown array that you are trying to recover. You are also given an array sums containing the values of all 2n subset sums of the unknown array (in no particular order).
Return the array ans of length n representing the unknown array. If multiple answers exist, return any of them.
An array sub is a subset of an array arr if sub can be obtained from arr by deleting some (possibly zero or all) elements of arr. The sum of the elements in sub is one possible subset sum of arr. The sum of an empty array is considered to be 0.
Note: Test cases are generated such that there will always be at least one correct answer.
Example 1:
Input: n = 3, sums = [-3,-2,-1,0,0,1,2,3]
Output: [1,2,-3]
Explanation: [1,2,-3] is able to achieve the given subset sums:
- []: sum is 0
- [1]: sum is 1
- [2]: sum is 2
- [1,2]: sum is 3
- [-3]: sum is -3
- [1,-3]: sum is -2
- [2,-3]: sum is -1
- [1,2,-3]: sum is 0
Note that any permutation of [1,2,-3] and also any permutation of [-1,-2,3] will also be accepted.Example 2:
Input: n = 2, sums = [0,0,0,0]
Output: [0,0]
Explanation: The only correct answer is [0,0].Example 3:
Input: n = 4, sums = [0,0,5,5,4,-1,4,9,9,-1,4,3,4,8,3,8]
Output: [0,-1,4,5]
Explanation: [0,-1,4,5] is able to achieve the given subset sums.
Constraints:
1 <= n <= 15sums.length == 2n-104 <= sums[i] <= 104
Approaches
2 approaches with complexity analysis and trade-offs.
This approach attempts to find the unknown array by trying all possibilities for its elements in a recursive, backtracking manner. It explores a search tree where each level corresponds to determining one element of the array.
Algorithm
- Define a recursive function
backtrack(k, sums_map)that attempts to findkelements. - The base case is
k=0. Ifsums_mapcontains only{0: 1}, a solution is found. - In the recursive step, iterate through all unique values
xin the currentsums_mapas candidates for an element. - For each candidate
x, attempt to partition the current sums into a new set of sumsSandS+x. - If a valid partition is found (i.e.,
Scontains 0 and all sums are accounted for), addxto the result and recurse:backtrack(k-1, S). - If the recursive call returns true, propagate the success. Otherwise, backtrack by removing
xand trying the next candidate.
Walkthrough
The core idea is to build the solution array element by element. We define a recursive function, say find(k, current_sums), which tries to find the remaining k elements given the current_sums that they must generate.
In each call, we iterate through all unique, non-zero values x in current_sums as a candidate for the next element. For each candidate x, we check if current_sums can be partitioned into two sets, S and S+x. If a valid partition exists (where S is a valid set of subset sums, meaning it contains 0), we add x to our potential answer and recurse with find(k-1, S). If the recursive call eventually succeeds, we have found a solution. If not, or if a valid partition for x cannot be found, we backtrack by removing x and trying the next candidate.
The base case for the recursion is when k=0. If current_sums is [0], it means we have successfully explained all sums, and a valid array has been found.
// This is a conceptual illustration. A full implementation would be complex and inefficient.class Solution { public int[] recoverArray(int n, int[] sums) { // The actual implementation would be very slow and likely time out. // It serves as a conceptual brute-force method. Map<Integer, Long> counts = new HashMap<>(); for (int s : sums) { counts.put(s, counts.getOrDefault(s, 0L) + 1); } List<Integer> ans = new ArrayList<>(); backtrack(n, counts, ans); return ans.stream().mapToInt(i -> i).toArray(); } private boolean backtrack(int k, Map<Integer, Long> currentCounts, List<Integer> ans) { if (k == 0) { return currentCounts.size() == 1 && currentCounts.getOrDefault(0, 0L) > 0; } List<Integer> candidates = new ArrayList<>(currentCounts.keySet()); for (int x : candidates) { if (x == 0) continue; Map<Integer, Long> nextCounts = tryPartition(currentCounts, x); if (nextCounts != null) { ans.add(x); if (backtrack(k - 1, nextCounts, ans)) { return true; } ans.remove(ans.size() - 1); // Backtrack } } return false; } private Map<Integer, Long> tryPartition(Map<Integer, Long> counts, int d) { if (d == 0) return null; Map<Integer, Long> tempCounts = new HashMap<>(counts); Map<Integer, Long> nextCounts = new HashMap<>(); List<Integer> sortedKeys = new ArrayList<>(tempCounts.keySet()); Collections.sort(sortedKeys); for (int s : sortedKeys) { if (tempCounts.get(s) > 0) { long sCount = tempCounts.get(s); int partner = s + d; if (tempCounts.getOrDefault(partner, 0L) < sCount) { return null; // Partition failed } tempCounts.put(s, 0L); tempCounts.put(partner, tempCounts.get(partner) - sCount); nextCounts.put(s, sCount); } } if (nextCounts.getOrDefault(0, 0L) == 0) return null; return nextCounts; }}Complexity
Time
Worst-case is exponential in `n` and the number of unique sums. The branching factor at each of the `n` levels of recursion can be up to `O(2^n)`, leading to a complexity far exceeding practical limits, something like `O((2^n)! * 2^n)`.
Space
O(n * 2^n) due to the recursion stack, where at each level a copy of the frequency map of sums (of size up to `2^n`) is stored.
Trade-offs
Pros
Conceptually simple to understand as it explores all possibilities.
Cons
Extremely inefficient due to the large branching factor.
Will time out for the given constraints (
nup to 15).
This efficient approach leverages a key insight into the structure of subset sums. By identifying one element x of the unknown array, the problem can be reduced to finding an array of size n-1 from a new set of sums half the size. A strong candidate for x can be found by observing the smallest sums.
Algorithm
- Start with the given
nandsums. - Repeat
ntimes to find each element:-
- Let the current size of the
sumsarray bem = 2^k.
- Let the current size of the
-
- Sort the
sumsarray.
- Sort the
-
- Calculate the candidate difference
d = sums[1] - sums[0].
- Calculate the candidate difference
-
- Create a frequency map of the numbers in
sums.
- Create a frequency map of the numbers in
-
- Greedily partition
sumsinto two new lists,S1andS2. Iterate throughsums; for each elementsnot yet used, addstoS1,s+dtoS2, and decrement their counts in the map.
- Greedily partition
-
- During the partition, check if
0is placed intoS1.
- During the partition, check if
-
- If
0was placed inS1, the recovered element isd. The nextsumsarray isS1.
- If
-
- Otherwise, the recovered element is
-d. The nextsumsarray isS2.
- Otherwise, the recovered element is
-
- Add the recovered element to the answer array.
-
- Return the final answer array.
Walkthrough
Let Sums_n be the set of 2^n subset sums. If we know one element x, then Sums_n is the union of Sums_{n-1} (subset sums of the other n-1 elements) and Sums_{n-1} + x (each sum from Sums_{n-1} increased by x).
This structure allows for a recursive solution. The main challenge is to find an element x. If we sort Sums_n, the difference d = sums[1] - sums[0] becomes a powerful candidate. It can be shown that either d or -d must be one of the elements in the unknown array.
The algorithm is as follows:
- Start with the full
sumsarray andn. - In a loop that runs
ntimes (or a recursive function): a. Let the current number of elements to find bekand the array of sums becurrent_sums(size2^k). b. Sortcurrent_sums. c. Calculate the differenced = current_sums[1] - current_sums[0]. d. Greedily partitioncurrent_sumsinto two lists,S1andS2, of size2^(k-1). Iterate through the sortedcurrent_sums; for each numbers, pair it withs+d. This is done efficiently with a frequency map. e. After partitioning, one list representsSums_{k-1}and the otherSums_{k-1} + x. TheSums_{k-1}set must contain0(for the empty subset). We check which list,S1orS2, contains0. f. IfS1contains0, then the element isd, andS1is the new set of sums for thek-1problem. g. IfS2contains0, then the element is-d, andS2is the new set of sums. h. Add the found element to the result and continue with the new, smaller set of sums. - Repeat until all
nelements are found.
import java.util.*; class Solution { public int[] recoverArray(int n, int[] sums) { Arrays.sort(sums); int[] ans = new int[n]; int ansIdx = 0; for (int i = n; i > 0; i--) { int m = 1 << i; int d = sums[1] - sums[0]; List<Integer> s1 = new ArrayList<>(); List<Integer> s2 = new ArrayList<>(); Map<Integer, Integer> counts = new HashMap<>(); for (int s : sums) { counts.put(s, counts.getOrDefault(s, 0) + 1); } boolean zeroInS1 = false; for (int j = 0; j < m; j++) { int s = sums[j]; if (counts.get(s) == 0) { continue; } s1.add(s); counts.put(s, counts.get(s) - 1); int partner = s + d; s2.add(partner); counts.put(partner, counts.get(partner) - 1); if (s == 0) { zeroInS1 = true; } } if (zeroInS1) { ans[ansIdx++] = d; sums = s1.stream().mapToInt(val -> val).toArray(); } else { ans[ansIdx++] = -d; sums = s2.stream().mapToInt(val -> val).toArray(); } } return ans; }}Complexity
Time
O(n * 2^n). The main loop runs `n` times. Inside the loop, for a sums array of size `m`, we sort (`O(m log m)`) and partition (`O(m)`). The total complexity is the sum of `O(2^k * k)` for `k` from `1` to `n`, which is dominated by the largest term, resulting in `O(n * 2^n)`.
Space
O(2^n). We need space to store the current `sums` array, the frequency map, and the two new lists `S1` and `S2` during partitioning. The space requirement halves at each step, so the peak usage is `O(2^n)`.
Trade-offs
Pros
Highly efficient and guaranteed to find a correct solution.
The logic is deterministic, avoiding a large search space.
Cons
The core insight (
d = sums[1] - sums[0]) is not immediately obvious.Requires careful implementation of the partitioning logic.
Solutions
Solution
class Solution {public int[] recoverArray(int n, int[] sums) { int m = 1 << 30; for (int x : sums) { m = Math.min(m, x); } m = -m; TreeMap<Integer, Integer> tm = new TreeMap<>(); for (int x : sums) { tm.merge(x + m, 1, Integer : : sum); } int[] ans = new int[n]; if (tm.merge(0, -1, Integer : : sum) == 0) { tm.remove(0); } ans[0] = tm.firstKey(); for (int i = 1; i < n; ++i) { for (int j = 0; j < 1 << i; ++j) { if ((j >> (i - 1) & 1) == 1) { int s = 0; for (int k = 0; k < i; ++k) { if (((j >> k) & 1) == 1) { s += ans[k]; } } if (tm.merge(s, -1, Integer : : sum) == 0) { tm.remove(s); } } } ans[i] = tm.firstKey(); } for (int i = 0; i < 1 << n; ++i) { int s = 0; for (int j = 0; j < n; ++j) { if (((i >> j) & 1) == 1) { s += ans[j]; } } if (s == m) { for (int j = 0; j < n; ++j) { if (((i >> j) & 1) == 1) { ans[j] *= -1; } } break; } } return ans; }}class Solution {public: vector<int> recoverArray(int n, vector<int> &sums) { int m = *min_element(sums.begin(), sums.end()); m = -m; multiset<int> st; for (int x : sums) { st.insert(x + m); } st.erase(st.begin()); vector<int> ans; ans.push_back(*st.begin()); for (int i = 1; i < n; ++i) { for (int j = 0; j < 1 << i; ++j) { if (j >> (i - 1) & 1) { int s = 0; for (int k = 0; k < i; ++k) { if (j >> k & 1) { s += ans[k]; } } st.erase(st.find(s)); } } ans.push_back(*st.begin()); } for (int i = 0; i < 1 << n; ++i) { int s = 0; for (int j = 0; j < n; ++j) { if (i >> j & 1) { s += ans[j]; } } if (s == m) { for (int j = 0; j < n; ++j) { if (i >> j & 1) { ans[j] = -ans[j]; } } break; } } return ans; }};from sortedcontainers import SortedList class Solution : def recoverArray ( self , n : int , sums : List [ int ]) -> List [ int ]: m = - min ( sums ) sl = SortedList ( x + m for x in sums ) sl . remove ( 0 ) ans = [ sl [ 0 ]] 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 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 return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.