Facebook Pixel

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] and goal = 5, you could choose subsequence [2, 3] with sum 5, giving an absolute difference of abs(5 - 5) = 0
  • If nums = [7, -9, 15, -2] and goal = -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.

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

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:

  1. Divide the array: Split nums into two roughly equal halves - left half and right half.

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

  3. 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 sum r from the right half such that l + r is closest to goal.

  4. Optimization with binary search: For each sum l from the left half, we need remaining = goal - l from the right half. To find the closest value to remaining 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 to remaining, and the largest value less than remaining.

  5. 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] and nums[n//2:]
  • Two sets (left and right) 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)
  • 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 for remaining 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

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 Evaluator

Example 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 size n/2, this creates 2^(n/2) subsets, taking O(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 takes O(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 and right 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 depth n/2, contributing O(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 of int 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More