1755. Closest Subsequence Sum
Problem Description
You are given an integer array nums
and an integer goal
. Your task is to find a subsequence of nums
whose sum is as close as possible to the goal
value.
A subsequence is formed by selecting some elements from the original array while maintaining their relative order. You can choose any combination of elements, including selecting all elements, no elements, or any subset in between.
The objective is to minimize the absolute difference between the sum of your chosen subsequence and the goal. In other words, if your subsequence has a sum of sum
, you want to minimize abs(sum - goal)
.
For example:
- If
nums = [1, 2, 3]
andgoal = 5
, you could choose subsequence[2, 3]
with sum 5, giving an absolute difference ofabs(5 - 5) = 0
- If
nums = [7, -9, 15, -2]
andgoal = -5
, you might choose subsequence[-9, -2]
with sum -11, or just[-2]
with sum -2, comparing which gives the smaller absolute difference to -5
The function should return the minimum possible value of abs(sum - goal)
that can be achieved by any valid subsequence of the input array.
Intuition
The naive approach would be to generate all possible subsequences and check which one gives the minimum absolute difference with the goal. However, with n
elements, we'd have 2^n
possible subsequences, which becomes computationally infeasible for larger arrays.
The key insight is to use a "meet in the middle" strategy. Instead of generating all 2^n
subsequences at once, we can split the array into two halves and generate subsequences for each half separately. This reduces our problem from 2^n
to 2^(n/2) + 2^(n/2) = 2 * 2^(n/2)
operations, which is a significant improvement.
Here's how we arrive at this approach:
-
Divide the array: Split
nums
into two roughly equal halves - left half and right half. -
Generate all possible sums: For each half, generate all possible subsequence sums. The left half gives us
2^(n/2)
possible sums, and similarly for the right half. -
The clever part: Any subsequence of the original array can be represented as a combination of one subsequence from the left half and one from the right half. So if we pick a sum
l
from the left half, we need to find the best matching sumr
from the right half such thatl + r
is closest togoal
. -
Optimization with binary search: For each sum
l
from the left half, we needremaining = goal - l
from the right half. To find the closest value toremaining
in the right half sums, we can sort the right half sums and use binary search. This allows us to quickly find the two candidates: the smallest value greater than or equal toremaining
, and the largest value less thanremaining
. -
Track the minimum: For each combination, calculate
abs((l + r) - goal)
and keep track of the minimum difference found.
This approach cleverly reduces an exponential problem to a more manageable complexity of O(2^(n/2) * log(2^(n/2)))
, making it feasible to solve even for moderately sized arrays.
Learn more about Two Pointers, Dynamic Programming, Bitmask and Sorting patterns.
Solution Approach
The implementation follows the "meet in the middle" strategy discussed earlier. Let's walk through the code step by step:
1. Split the Array
n = len(nums)
left = set()
right = set()
self.getSubSeqSum(0, 0, nums[: n // 2], left)
self.getSubSeqSum(0, 0, nums[n // 2 :], right)
- The array is divided into two halves:
nums[:n//2]
andnums[n//2:]
- Two sets (
left
andright
) are used to store all possible subsequence sums from each half - Sets are used to automatically handle duplicate sums
2. Generate All Subsequence Sums
def getSubSeqSum(self, i: int, curr: int, arr: List[int], result: Set[int]):
if i == len(arr):
result.add(curr)
return
self.getSubSeqSum(i + 1, curr, arr, result)
self.getSubSeqSum(i + 1, curr + arr[i], arr, result)
- This recursive function generates all possible subsequence sums
- For each element at index
i
, we have two choices:- Don't include it:
getSubSeqSum(i + 1, curr, arr, result)
- Include it:
getSubSeqSum(i + 1, curr + arr[i], arr, result)
- Don't include it:
- When we reach the end of the array (
i == len(arr)
), we add the current sum to the result set - This generates
2^(n/2)
sums for each half
3. Find the Closest Sum Using Binary Search
result = inf
right = sorted(right)
rl = len(right)
for l in left:
remaining = goal - l
idx = bisect_left(right, remaining)
if idx < rl:
result = min(result, abs(remaining - right[idx]))
if idx > 0:
result = min(result, abs(remaining - right[idx - 1]))
- Sort the right set to enable binary search
- For each sum
l
from the left half:- Calculate
remaining = goal - l
(what we need from the right half) - Use
bisect_left
to find the insertion point forremaining
in the sorted right array - Check two candidates:
right[idx]
: The smallest value ≥remaining
(if it exists)right[idx - 1]
: The largest value <remaining
(if it exists)
- Update the result with the minimum absolute difference found
- Calculate
Time Complexity Analysis:
- Generating subsequence sums:
O(2^(n/2))
for each half - Sorting the right array:
O(2^(n/2) * log(2^(n/2))) = O(n * 2^(n/2))
- Binary search for each left sum:
O(2^(n/2) * log(2^(n/2)))
- Overall:
O(n * 2^(n/2))
Space Complexity: O(2^(n/2))
to store all the subsequence sums
This approach effectively reduces the problem from O(2^n)
to O(n * 2^(n/2))
, making it feasible for arrays with up to approximately 40 elements.
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 a concrete example:
Input: nums = [7, -9, 15, -2]
, goal = -5
Step 1: Split the array into two halves
- Left half:
[7, -9]
- Right half:
[15, -2]
Step 2: Generate all subsequence sums for each half
For the left half [7, -9]
:
- Choose neither: sum =
0
- Choose only
7
: sum =7
- Choose only
-9
: sum =-9
- Choose both
7
and-9
: sum =7 + (-9) = -2
- Left sums:
{0, 7, -9, -2}
For the right half [15, -2]
:
- Choose neither: sum =
0
- Choose only
15
: sum =15
- Choose only
-2
: sum =-2
- Choose both
15
and-2
: sum =15 + (-2) = 13
- Right sums:
{0, 15, -2, 13}
Step 3: Sort the right sums for binary search
- Sorted right sums:
[-2, 0, 13, 15]
Step 4: For each left sum, find the best matching right sum
For l = 0
from left:
- Need
remaining = goal - l = -5 - 0 = -5
from right - Binary search for
-5
in[-2, 0, 13, 15]
gives insertion index 0 - Check candidates:
right[0] = -2
: difference =|(-5) - (-2)| = 3
- Total sum =
0 + (-2) = -2
, distance from goal =|(-2) - (-5)| = 3
For l = 7
from left:
- Need
remaining = -5 - 7 = -12
from right - Binary search for
-12
in[-2, 0, 13, 15]
gives insertion index 0 - Check candidates:
right[0] = -2
: difference =|(-12) - (-2)| = 10
- Total sum =
7 + (-2) = 5
, distance from goal =|5 - (-5)| = 10
For l = -9
from left:
- Need
remaining = -5 - (-9) = 4
from right - Binary search for
4
in[-2, 0, 13, 15]
gives insertion index 2 - Check candidates:
right[2] = 13
: difference =|4 - 13| = 9
right[1] = 0
: difference =|4 - 0| = 4
- Best match is with
0
: Total sum =-9 + 0 = -9
, distance from goal =|(-9) - (-5)| = 4
For l = -2
from left:
- Need
remaining = -5 - (-2) = -3
from right - Binary search for
-3
in[-2, 0, 13, 15]
gives insertion index 0 - Check candidates:
right[0] = -2
: difference =|(-3) - (-2)| = 1
- Total sum =
-2 + (-2) = -4
, distance from goal =|(-4) - (-5)| = 1
Step 5: Return the minimum difference
The minimum difference found is 1
, achieved by selecting the subsequence [-2, -2]
(both -2
values from the original array) with sum -4
.
Solution Implementation
1class Solution:
2 def minAbsDifference(self, nums: List[int], goal: int) -> int:
3 """
4 Find the minimum absolute difference between the sum of any subsequence and the goal.
5 Uses meet-in-the-middle technique to reduce time complexity from O(2^n) to O(2^(n/2) * n).
6 """
7 n = len(nums)
8
9 # Split array into two halves for meet-in-the-middle approach
10 left_sums = set()
11 right_sums = set()
12
13 # Generate all possible subsequence sums for each half
14 self.getSubSeqSum(0, 0, nums[:n // 2], left_sums)
15 self.getSubSeqSum(0, 0, nums[n // 2:], right_sums)
16
17 # Sort right sums for binary search
18 sorted_right_sums = sorted(right_sums)
19 right_length = len(sorted_right_sums)
20
21 min_difference = float('inf')
22
23 # For each sum from left half, find the best matching sum from right half
24 for left_sum in left_sums:
25 # We need to find right_sum such that left_sum + right_sum is closest to goal
26 target = goal - left_sum
27
28 # Binary search for the closest value to target in sorted_right_sums
29 insertion_point = bisect_left(sorted_right_sums, target)
30
31 # Check the value at or after insertion point (ceiling value)
32 if insertion_point < right_length:
33 min_difference = min(min_difference, abs(target - sorted_right_sums[insertion_point]))
34
35 # Check the value before insertion point (floor value)
36 if insertion_point > 0:
37 min_difference = min(min_difference, abs(target - sorted_right_sums[insertion_point - 1]))
38
39 return min_difference
40
41 def getSubSeqSum(self, index: int, current_sum: int, array: List[int], result_set: Set[int]) -> None:
42 """
43 Recursively generate all possible subsequence sums.
44
45 Args:
46 index: Current position in the array
47 current_sum: Sum accumulated so far
48 array: The array to generate subsequences from
49 result_set: Set to store all possible sums
50 """
51 # Base case: reached end of array
52 if index == len(array):
53 result_set.add(current_sum)
54 return
55
56 # Option 1: Exclude current element
57 self.getSubSeqSum(index + 1, current_sum, array, result_set)
58
59 # Option 2: Include current element
60 self.getSubSeqSum(index + 1, current_sum + array[index], array, result_set)
61
1class Solution {
2 /**
3 * Find the minimum absolute difference between the sum of any subsequence and the goal.
4 * Uses meet-in-the-middle technique to reduce time complexity from O(2^n) to O(2^(n/2) * log(2^(n/2))).
5 *
6 * @param nums the input array
7 * @param goal the target sum to get close to
8 * @return minimum absolute difference between any subsequence sum and goal
9 */
10 public int minAbsDifference(int[] nums, int goal) {
11 int n = nums.length;
12
13 // Split array into two halves and generate all possible sums for each half
14 List<Integer> leftSums = new ArrayList<>();
15 List<Integer> rightSums = new ArrayList<>();
16
17 // Generate all possible sums for left half [0, n/2)
18 generateAllSums(nums, leftSums, 0, n / 2, 0);
19
20 // Generate all possible sums for right half [n/2, n)
21 generateAllSums(nums, rightSums, n / 2, n, 0);
22
23 // Sort right sums for binary search
24 rightSums.sort(Integer::compareTo);
25
26 int minDifference = Integer.MAX_VALUE;
27
28 // For each sum from left half, find the closest sum from right half
29 for (Integer leftSum : leftSums) {
30 // We need rightSum such that leftSum + rightSum is closest to goal
31 // So we search for rightSum closest to (goal - leftSum)
32 int target = goal - leftSum;
33
34 // Binary search to find the position where target should be inserted
35 int left = 0;
36 int right = rightSums.size();
37
38 while (left < right) {
39 int mid = (left + right) >> 1;
40 if (rightSums.get(mid) < target) {
41 left = mid + 1;
42 } else {
43 right = mid;
44 }
45 }
46
47 // Check the element at position 'left' (first element >= target)
48 if (left < rightSums.size()) {
49 int currentSum = leftSum + rightSums.get(left);
50 minDifference = Math.min(minDifference, Math.abs(goal - currentSum));
51 }
52
53 // Check the element before position 'left' (last element < target)
54 if (left > 0) {
55 int currentSum = leftSum + rightSums.get(left - 1);
56 minDifference = Math.min(minDifference, Math.abs(goal - currentSum));
57 }
58 }
59
60 return minDifference;
61 }
62
63 /**
64 * Generate all possible sums by including or excluding each element.
65 * Uses DFS to explore all 2^(endIndex-startIndex) possibilities.
66 *
67 * @param nums the input array
68 * @param sums list to store all generated sums
69 * @param currentIndex current position in the array
70 * @param endIndex boundary index (exclusive)
71 * @param currentSum accumulated sum so far
72 */
73 private void generateAllSums(int[] nums, List<Integer> sums, int currentIndex,
74 int endIndex, int currentSum) {
75 // Base case: reached the end of the range
76 if (currentIndex == endIndex) {
77 sums.add(currentSum);
78 return;
79 }
80
81 // Option 1: Exclude current element
82 generateAllSums(nums, sums, currentIndex + 1, endIndex, currentSum);
83
84 // Option 2: Include current element
85 generateAllSums(nums, sums, currentIndex + 1, endIndex, currentSum + nums[currentIndex]);
86 }
87}
88
1class Solution {
2public:
3 int minAbsDifference(vector<int>& nums, int goal) {
4 int n = nums.size();
5
6 // Split array into two halves and generate all possible sums for each half
7 vector<int> leftSums;
8 vector<int> rightSums;
9
10 // Generate all possible sums for left half [0, n/2)
11 generateSubsetSums(nums, leftSums, 0, n / 2, 0);
12
13 // Generate all possible sums for right half [n/2, n)
14 generateSubsetSums(nums, rightSums, n / 2, n, 0);
15
16 // Sort right sums for binary search
17 sort(rightSums.begin(), rightSums.end());
18
19 int minDifference = INT_MAX;
20
21 // For each sum in left half, find the best matching sum in right half
22 for (int leftSum : leftSums) {
23 // We need to find rightSum such that leftSum + rightSum is closest to goal
24 int target = goal - leftSum;
25
26 // Binary search to find the smallest element >= target
27 int left = 0;
28 int right = rightSums.size();
29
30 while (left < right) {
31 int mid = (left + right) >> 1;
32 if (rightSums[mid] < target) {
33 left = mid + 1;
34 } else {
35 right = mid;
36 }
37 }
38
39 // Check the element at or after target (if exists)
40 if (left < rightSums.size()) {
41 minDifference = min(minDifference, abs(target - rightSums[left]));
42 }
43
44 // Check the element just before target (if exists)
45 if (left > 0) {
46 minDifference = min(minDifference, abs(target - rightSums[left - 1]));
47 }
48 }
49
50 return minDifference;
51 }
52
53private:
54 // Generate all possible subset sums from index startIdx to endIdx (exclusive)
55 void generateSubsetSums(vector<int>& nums, vector<int>& sums,
56 int currentIdx, int endIdx, int currentSum) {
57 // Base case: reached the end of the range
58 if (currentIdx == endIdx) {
59 sums.emplace_back(currentSum);
60 return;
61 }
62
63 // Choice 1: Don't include current element
64 generateSubsetSums(nums, sums, currentIdx + 1, endIdx, currentSum);
65
66 // Choice 2: Include current element
67 generateSubsetSums(nums, sums, currentIdx + 1, endIdx, currentSum + nums[currentIdx]);
68 }
69};
70
1/**
2 * Finds the minimum absolute difference between any subset sum and the goal
3 * Uses meet-in-the-middle approach to optimize from O(2^n) to O(2^(n/2))
4 * @param nums - Array of integers to form subsets from
5 * @param goal - Target value to get closest to
6 * @returns Minimum absolute difference between any subset sum and goal
7 */
8function minAbsDifference(nums: number[], goal: number): number {
9 const n = nums.length;
10
11 // Split array into two halves and generate all possible sums for each half
12 const leftSums: number[] = [];
13 const rightSums: number[] = [];
14
15 // Generate all possible sums for left half [0, n/2)
16 generateSubsetSums(nums, leftSums, 0, Math.floor(n / 2), 0);
17
18 // Generate all possible sums for right half [n/2, n)
19 generateSubsetSums(nums, rightSums, Math.floor(n / 2), n, 0);
20
21 // Sort right sums for binary search
22 rightSums.sort((a, b) => a - b);
23
24 let minDifference = Number.MAX_SAFE_INTEGER;
25
26 // For each sum in left half, find the best matching sum in right half
27 for (const leftSum of leftSums) {
28 // We need to find rightSum such that leftSum + rightSum is closest to goal
29 const target = goal - leftSum;
30
31 // Binary search to find the smallest element >= target
32 let left = 0;
33 let right = rightSums.length;
34
35 while (left < right) {
36 const mid = left + Math.floor((right - left) / 2);
37 if (rightSums[mid] < target) {
38 left = mid + 1;
39 } else {
40 right = mid;
41 }
42 }
43
44 // Check the element at or after target (if exists)
45 if (left < rightSums.length) {
46 minDifference = Math.min(minDifference, Math.abs(target - rightSums[left]));
47 }
48
49 // Check the element just before target (if exists)
50 if (left > 0) {
51 minDifference = Math.min(minDifference, Math.abs(target - rightSums[left - 1]));
52 }
53 }
54
55 return minDifference;
56}
57
58/**
59 * Recursively generates all possible subset sums from a range of indices
60 * @param nums - Original array of numbers
61 * @param sums - Array to store all generated subset sums
62 * @param currentIdx - Current index being processed
63 * @param endIdx - End index (exclusive) of the range to process
64 * @param currentSum - Current accumulated sum
65 */
66function generateSubsetSums(
67 nums: number[],
68 sums: number[],
69 currentIdx: number,
70 endIdx: number,
71 currentSum: number
72): void {
73 // Base case: reached the end of the range
74 if (currentIdx === endIdx) {
75 sums.push(currentSum);
76 return;
77 }
78
79 // Choice 1: Don't include current element
80 generateSubsetSums(nums, sums, currentIdx + 1, endIdx, currentSum);
81
82 // Choice 2: Include current element
83 generateSubsetSums(nums, sums, currentIdx + 1, endIdx, currentSum + nums[currentIdx]);
84}
85
Time and Space Complexity
Time Complexity: O(2^(n/2) * log(2^(n/2)))
which simplifies to O(2^(n/2) * n)
The algorithm uses a meet-in-the-middle approach:
getSubSeqSum
generates all possible subset sums for each half of the array. For an array of sizen/2
, this creates2^(n/2)
subsets, takingO(2^(n/2))
time for each half.- Sorting the right set takes
O(2^(n/2) * log(2^(n/2)))
=O(2^(n/2) * n/2)
. - The final loop iterates through all
2^(n/2)
elements in the left set, and for each element performs a binary search on the sorted right set, which takesO(log(2^(n/2)))
=O(n/2)
time. - Total:
O(2^(n/2))
+O(2^(n/2))
+O(2^(n/2) * n/2)
+O(2^(n/2) * n/2)
=O(2^(n/2) * n)
Space Complexity: O(2^(n/2))
- Both
left
andright
sets store all possible subset sums from their respective halves of the array. - Each set can contain at most
2^(n/2)
elements. - The recursion stack for
getSubSeqSum
goes to depthn/2
, contributingO(n/2)
space. - The sorted list created from the right set also takes
O(2^(n/2))
space. - Total:
O(2^(n/2))
+O(2^(n/2))
+O(n/2)
+O(2^(n/2))
=O(2^(n/2))
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Array Values
One critical pitfall is not considering integer overflow when array elements or the goal value are very large. In Python, this is handled automatically, but in languages like Java or C++, the sum calculations could overflow.
Problem Example:
nums = [10**9, 10**9, 10**9, 10**9] # Large values goal = 4 * 10**9
Solution:
- In Python: No action needed as Python handles arbitrary precision integers
- In Java/C++: Use
long
type instead ofint
for sums and calculations - Consider using modular arithmetic or careful bounds checking if needed
2. Empty Array Edge Case
The code doesn't explicitly handle the case when nums
is empty, which could lead to unexpected behavior.
Problem Example:
nums = [] goal = 5 # The code would still work but it's not immediately clear
Solution:
def minAbsDifference(self, nums: List[int], goal: int) -> int:
if not nums:
return abs(goal) # Empty subsequence has sum 0
# ... rest of the code
3. Memory Limit Exceeded for Large Arrays
When the array size approaches 40, each half generates 2^20 ≈ 1 million subsequence sums. This could cause memory issues.
Problem Example:
nums = [1] * 40 # Will generate 2^20 sums for each half
Solution: Implement iterative generation with pruning:
def getSubSeqSumIterative(self, arr: List[int]) -> Set[int]:
sums = {0}
for num in arr:
new_sums = set()
for s in sums:
new_sum = s + num
# Optional: Add pruning logic here if possible
new_sums.add(new_sum)
sums.update(new_sums)
return sums
4. Duplicate Values Not Being Leveraged
While using sets eliminates duplicate sums, arrays with many duplicate values still go through all 2^(n/2) recursive calls unnecessarily.
Problem Example:
nums = [5, 5, 5, 5, 5, 5] # Many duplicates # Still makes 2^3 = 8 recursive calls per half even though many produce same sums
Solution: Pre-process to count duplicates and generate sums more efficiently:
def optimizedSubSeqSum(self, arr: List[int]) -> Set[int]:
from collections import Counter
counts = Counter(arr)
sums = {0}
for value, count in counts.items():
new_sums = set()
for existing_sum in sums:
# Add all possible counts of this value (0 to count)
for i in range(count + 1):
new_sums.add(existing_sum + value * i)
sums = new_sums
return sums
5. Not Handling the Case When Goal is Achievable
The algorithm doesn't short-circuit when it finds an exact match (difference = 0), continuing to check all combinations unnecessarily.
Solution: Add early termination:
for left_sum in left_sums: target = goal - left_sum # Early termination if exact match found if target in sorted_right_sums: return 0 insertion_point = bisect_left(sorted_right_sums, target) # ... rest of the binary search logic
Which algorithm should you use to find a node that is close to the root of the tree?
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
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!