1755. Closest Subsequence Sum


Problem Description

In this problem, you are given an array of integers nums and an integer goal. The objective is to select a subsequence from the array such that the absolute difference between the sum of the selected subsequence and the goal is minimized. In other words, you want to find a subsequence whose sum is as close as possible to the goal. A subsequence of an array can be derived by omitting any number of elements from the array, which could potentially be none or all of them.

Intuition

The direct approach to solve this problem would involve generating all possible subsequences of the given array nums and calculating the sum for each subsequence to see how close it is to the goal. However, this approach is not feasible because the number of possible subsequences of an array is 2^n, which would result in exponential time complexity making it impractical for large arrays.

The intuition behind the provided solution is to use the "meet in the middle" strategy. This involves dividing the array nums into two nearly equal halves and then separately generating all possible sums of subsequences for each half. Once you have all subset sums from both halves, you can sort one of the halves (for example, the right half) to then use binary search to quickly find the best match for each subset sum from the other half (the left half). This way, you have reduced the original problem, which would have a complexity of O(2^n), into two subproblems each having a complexity of O(2^(n/2)), which is significantly more manageable.

The minAbsDifference function first generates all possible subset sums for both halves of the array using the helper function getSubSeqSum. It stores these sums in two sets, left and right. After that, it sorts the sums from the right half to allow efficient searching. For each sum in the left set, the function computes the complement value needed to reach the goal. Using binary search, it then checks whether a sum in the right set exists that is close to this complement value. The result is the smallest absolute difference found during this pairing process between left and right subset sums.

Learn more about Two Pointers, Dynamic Programming and Bitmask patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece

Which of the following array represent a max heap?

Solution Approach

The solution approach consists of the following key parts:

  1. Divide the array into two halves: First, the original array nums is split into two halves. This is a crucial step in the "meet in the middle" strategy.

  2. Generate all possible sums of both halves: The method getSubSeqSum is recursively called to calculate all feasible subset sums for the two halves of the array, which are stored in the left and right sets. This is done through classic backtracking – for each element, we can either choose to include it in the current subset or not, leading to two recursive calls.

    The pseudo-code for subset sum generation would be similar to:

    1function getSubSeqSum(index, currentSum, array, resultSet):
    2    if index == length of array:
    3        add currentSum to resultSet
    4        return
    5    end if
    6
    7    // Don't include the current element
    8    call getSubSeqSum(index + 1, currentSum, array, resultSet)
    9
    10    // Include the current element
    11    call getSubSeqSum(index + 1, currentSum + array[index], array, resultSet)
    12end function
  3. Sort the sums of one half and use binary search: After the generation of all subset sums for both halves, the sums from the right half are sorted to leverage binary search. The binary search allows us to find the closest element in right to the goal - left_sum, giving us the potential minimum difference.

    The logic for binary search comparison is as follows:

    1for each sum in left set:
    2    compute remaining = goal - sum
    3    find the index of the minimum element that is greater than or equal to remaining in the right
    4    if such an element exists, update result to minimum of result or absolute difference between that element and remaining
    5    if there is an element less than remaining (index > 0), update result to minimum of result or absolute difference between that element and remaining
    6end for
  4. Calculate and return the result: The overall minimum difference is tracked in the result variable, which is initialized with infinity (inf). It gets updated whenever a smaller absolute difference is found between a pair of left and right subset sums with respect to the goal.

By exploiting the "meet in the middle" strategy and binary search, we manage to reduce the time complexity of the problem from exponential to O(n * 2^(n/2)), which is much more efficient for inputs within the problem's constraints.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?

Example Walkthrough

Let's assume we have the following input:

nums = [1, 2, 3, 4] and goal = 6.

Following the solution approach described:

  1. Divide the array into two halves: We split nums into left_half = [1, 2] and right_half = [3, 4].

  2. Generate all possible sums of both halves: We use the getSubSeqSum method.

    For the left_half:

    1Using index 0, with currentSum 0 -> left {0}
    2Include number at index 0 -> left {0, 1}
    3Using index 1, with currentSum 1 -> left {0, 1}
    4Include number at index 1 -> left {0, 1, 3}
    5Repeat without including number 1 -> left {0, 1, 3, 2}
    6Include number at index 1 -> left {0, 1, 3, 2, 3}

    So the possible sums for left_half are {0, 1, 2, 3}.

    For the right_half:

    1Using index 0, with currentSum 0 -> right {0}
    2Include number at index 0 -> right {0, 3}
    3Using index 1, with currentSum 3 -> right {0, 3}
    4Include number at index 1 -> right {0, 3, 7}
    5Repeat without including number 3 -> right {0, 3, 7, 4}
    6Include number at index 1 -> right {0, 3, 7, 4, 7}

    So the possible sums for right_half are {0, 3, 4, 7}.

  3. Sort the sums of one half and use binary search: We sort the right_half sums to {0, 3, 4, 7} (although it's already sorted in this example).

    We perform binary searches for complement values (goal - left_sum) for each sum in the left_half sums.

    1left_sum = 0, remaining = goal - 0 = 6, closest in right is 7 (absolute difference 1)
    2left_sum = 1, remaining = goal - 1 = 5, closest in right is 4 or 7 (absolute differences 1 and 2)
    3left_sum = 2, remaining = goal - 2 = 4, closest in right is 4 (absolute difference 0)
    4left_sum = 3, remaining = goal - 3 = 3, closest in right is 3 (absolute difference 0)
  4. Calculate and return the result: From the binary search steps, we find that the smallest absolute difference is 0, which can be achieved with left_sum of 2 and right_sum of 4 or left_sum of 3 and right_sum of 3. Hence the minimum absolute difference between any subsequence sum and the goal is 0.

By using the described strategy, the problem that had a potentially exponential complexity is tackled in a much more manageable way, making it possible to solve efficiently even with larger inputs.

Solution Implementation

1from typing import List, Set
2from bisect import bisect_left
3
4class Solution:
5    def minAbsDifference(self, nums: List[int], goal: int) -> int:
6        # Split the array into two halves for separate processing
7        n = len(nums)
8        left_half_sums = set()
9        right_half_sums = set()
10
11        # Generate all possible sums of subsets for both halves
12        self._get_subset_sums(0, 0, nums[:n // 2], left_half_sums)
13        self._get_subset_sums(0, 0, nums[n // 2:], right_half_sums)
14
15        # Initialize the result to infinity (unbounded)
16        result = float('inf')
17        # Sort the sums generated from the right half for binary search
18        right_half_sums = sorted(right_half_sums)
19        right_half_len = len(right_half_sums)
20
21        # Iterate through every sum of the left half
22        for left_sum in left_half_sums:
23            remaining = goal - left_sum
24            # Find the closest sum in the right half to the remaining goal
25            idx = bisect_left(right_half_sums, remaining)
26
27            # If the index is within the bounds, check if the sum reduces the absolute difference
28            if idx < right_half_len:
29                result = min(result, abs(remaining - right_half_sums[idx]))
30
31            # Also check the number immediately before the found index to ensure minimum difference
32            if idx > 0:
33                result = min(result, abs(remaining - right_half_sums[idx - 1]))
34
35        # Return the minimum absolute difference found
36        return result
37
38    def _get_subset_sums(self, index: int, current_sum: int, array: List[int], result_set: Set[int]):
39        """Helper method to calculate all possible subset sums of a given array."""
40        # If we've reached the end of the array, add the current sum to the result set
41        if index == len(array):
42            result_set.add(current_sum)
43            return
44
45        # Recursive call to include the current index element and to exclude it, respectively
46        self._get_subset_sums(index + 1, current_sum, array, result_set)
47        self._get_subset_sums(index + 1, current_sum + array[index], array, result_set)
48
1class Solution {
2    public int minAbsDifference(int[] nums, int goal) {
3        // Divide the array into two halves
4        int n = nums.length;
5        List<Integer> leftSums = new ArrayList<>();
6        List<Integer> rightSums = new ArrayList<>();
7
8        // Generate all possible sums in the first half of the array
9        generateSums(nums, leftSums, 0, n / 2, 0);
10        // Generate all possible sums in the second half of the array
11        generateSums(nums, rightSums, n / 2, n, 0);
12
13        // Sort the list of sums from the right half for binary search
14        rightSums.sort(Integer::compareTo);
15
16        // Initialize result with the highest possible value of integer
17        int result = Integer.MAX_VALUE;
18
19        // Iterate through each sum in the left half and use binary search to find
20        // the closest pair in the right half that makes the sum close to the goal
21        for (Integer leftSum : leftSums) {
22            int target = goal - leftSum;
23            int left = 0, right = rightSums.size();
24            while (left < right) {
25                int mid = (left + right) >> 1;
26                if (rightSums.get(mid) < target) {
27                    left = mid + 1;
28                } else {
29                    right = mid;
30                }
31            }
32            // Check against the element on the right
33            if (left < rightSums.size()) {
34                result = Math.min(result, Math.abs(target - rightSums.get(left)));
35            }
36            // Check against the element on the left
37            if (left > 0) {
38                result = Math.min(result, Math.abs(target - rightSums.get(left - 1)));
39            }
40        }
41
42        return result;
43    }
44
45    // Helper method to recursively generate all possible subset sums
46    private void generateSums(int[] nums, List<Integer> sums, int start, int end, int currentSum) {
47        if (start == end) {
48            sums.add(currentSum);
49            return;
50        }
51
52        // Don't include the current element
53        generateSums(nums, sums, start + 1, end, currentSum);
54        // Include the current element
55        generateSums(nums, sums, start + 1, end, currentSum + nums[start]);
56    }
57}
58
1class Solution {
2public:
3    int minAbsDifference(vector<int>& nums, int goal) {
4        int numsSize = nums.size(); // The size of the input array
5      
6        // Two vectors to store the subsets' sum for left and right subarrays
7        vector<int> leftSums;
8        vector<int> rightSums;
9      
10        // Generate all possible sums for the left and right halves
11        generateSubsetsSum(nums, leftSums, 0, numsSize / 2, 0);
12        generateSubsetsSum(nums, rightSums, numsSize / 2, numsSize, 0);
13
14        // Sort the sums of the right subarray to utilize binary search later
15        sort(rightSums.begin(), rightSums.end());
16      
17        // This variable will hold the minimum absolute difference
18        int minDiff = INT_MAX;
19
20        // For each sum in the left subarray, look for the closest sum in the right subarray
21        // such that the sum of both is closest to the given goal
22        for (int sumLeft : leftSums) {
23            int target = goal - sumLeft; // The required sum from the right subarray
24          
25            // Perform binary search on rightSums to find an approximation of target
26            int leftIndex = 0, rightIndex = rightSums.size();
27            while (leftIndex < rightIndex) {
28                int midIndex = (leftIndex + rightIndex) / 2;
29                if (rightSums[midIndex] < target) {
30                    leftIndex = midIndex + 1;
31                } else {
32                    rightIndex = midIndex;
33                }
34            }
35          
36            // Update minDiff with the closer of two candidates
37            if (leftIndex < rightSums.size()) {
38                minDiff = min(minDiff, abs(target - rightSums[leftIndex]));
39            }
40            if (leftIndex > 0) {
41                minDiff = min(minDiff, abs(target - rightSums[leftIndex - 1]));
42            }
43        }
44
45        // Return the minimum absolute difference found
46        return minDiff;
47    }
48
49private:
50    // Utility function to generate all possible subset sums using DFS
51    void generateSubsetsSum(vector<int>& nums, vector<int>& sums, int startIndex, int endIndex, int currentSum) {
52        // Base case: If starting index reached the end index, add the currentSum to sums
53        if (startIndex == endIndex) {
54            sums.push_back(currentSum);
55            return;
56        }
57
58        // Exclude the current element and proceed to the next
59        generateSubsetsSum(nums, sums, startIndex + 1, endIndex, currentSum);
60      
61        // Include the current element and proceed to the next
62        generateSubsetsSum(nums, sums, startIndex + 1, endIndex, currentSum + nums[startIndex]);
63    }
64};
65
1// Utility function to generate all possible subset sums using DFS
2function generateSubsetsSum(nums: number[], sums: number[], startIndex: number, endIndex: number, currentSum: number): void {
3    // Base case: If starting index reached the end index, add the currentSum to sums
4    if (startIndex === endIndex) {
5        sums.push(currentSum);
6        return;
7    }
8
9    // Exclude the current element and proceed to the next
10    generateSubsetsSum(nums, sums, startIndex + 1, endIndex, currentSum);
11  
12    // Include the current element and proceed to the next
13    generateSubsetsSum(nums, sums, startIndex + 1, endIndex, currentSum + nums[startIndex]);
14}
15
16// The main function to find the minimum absolute difference to the goal
17function minAbsDifference(nums: number[], goal: number): number {
18    let numsSize = nums.length; // The size of the input array
19  
20    // Two arrays to store the subsets' sum for left and right partitions
21    let leftSums: number[] = [];
22    let rightSums: number[] = [];
23  
24    // Generate all possible sums for the left and right halves
25    generateSubsetsSum(nums, leftSums, 0, numsSize / 2, 0);
26    generateSubsetsSum(nums, rightSums, numsSize / 2, numsSize, 0);
27
28    // Sort the sums of the right partition to utilize binary search later
29    rightSums.sort((a, b) => a - b);
30  
31    // This variable will hold the minimum absolute difference
32    let minDiff = Number.MAX_SAFE_INTEGER;
33
34    // For each sum in the left partition, look for the closest sum in the right partition
35    for (let sumLeft of leftSums) {
36        let target = goal - sumLeft; // The required sum from the right partition
37      
38        // Perform binary search on rightSums to find an approximation of target
39        let leftIndex = 0;
40        let rightIndex = rightSums.length;
41        while (leftIndex < rightIndex) {
42            let midIndex = Math.floor((leftIndex + rightIndex) / 2);
43            if (rightSums[midIndex] < target) {
44                leftIndex = midIndex + 1;
45            } else {
46                rightIndex = midIndex;
47            }
48        }
49      
50        // Update minDiff with the closer of two candidates
51        if (leftIndex < rightSums.length) {
52            minDiff = Math.min(minDiff, Math.abs(target - rightSums[leftIndex]));
53        }
54        if (leftIndex > 0) {
55            minDiff = Math.min(minDiff, Math.abs(target - rightSums[leftIndex - 1]));
56        }
57    }
58
59    // Return the minimum absolute difference found
60    return minDiff;
61}
62
Not Sure What to Study? Take the 2-min Quiz

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Time and Space Complexity

The time complexity and space complexity analysis for the minAbsDifference function is as follows:

Time Complexity:

  • The function getSubSeqSum generates all possible subsets' sums for the input subarrays. This function is called recursively for each element in the input subarray. There are 2^n subsets possible for an array with n elements, resulting in a time complexity of O(2^n) for creating all subsets for an array.
  • Since getSubSeqSum is first called with half the size of the input array O(2^(n/2)), and then with the remaining half O(2^(n/2)), the total time for the subset sum generation steps for both halves is O(2*(2^(n/2))) which simplifies to O(2^(n/2 + 1)).
  • The set right is sorted afterwards which has at most 2^(n/2) elements leading to a sort time complexity of O(2^(n/2) * log(2^(n/2))) which simplifies to O((n/2) * 2^(n/2)).
  • Next, the for loop iterates over all elements of left, and for each element, a binary search is performed on right using bisect_left. This gives a time complexity of O(2^(n/2) * log(2^(n/2))) for the loop which simplifies to O((n/2) * 2^(n/2)).

The overall time complexity can be expressed as O(2^(n/2 + 1) + (n/2) * 2^(n/2) + (n/2) * 2^(n/2)) which simplifies to O(n * 2^(n/2)).

Space Complexity:

  • The space required is to store all subset sums for both halves and considering the deepest recursion call stack. This leads to O(2^(n/2)) for left and O(2^(n/2)) for right sets separately.
  • The recursion call stack of the function getSubSeqSum will go to a maximum depth of n/2 in both halves, so the space used by the call stack is O(n/2 + n/2) which simplifies to O(n).

Hence, the space complexity can be viewed as the maximum space consumed at any point, giving O(2^(n/2) + 2^(n/2) + n) which simplifies to O(2^(n/2) + n).

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

A heap is a ...?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.

←
↑TA đŸ‘šâ€đŸ«