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.
Solution Approach
The solution approach consists of the following key parts:
-
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. -
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 theleft
andright
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:
function getSubSeqSum(index, currentSum, array, resultSet): if index == length of array: add currentSum to resultSet return end if // Don't include the current element call getSubSeqSum(index + 1, currentSum, array, resultSet) // Include the current element call getSubSeqSum(index + 1, currentSum + array[index], array, resultSet) end function
-
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 thegoal - left_sum
, giving us the potential minimum difference.The logic for binary search comparison is as follows:
for each sum in left set: compute remaining = goal - sum find the index of the minimum element that is greater than or equal to remaining in the right if such an element exists, update result to minimum of result or absolute difference between that element and remaining if there is an element less than remaining (index > 0), update result to minimum of result or absolute difference between that element and remaining end for
-
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 thegoal
.
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.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's assume we have the following input:
nums = [1, 2, 3, 4]
and goal = 6
.
Following the solution approach described:
-
Divide the array into two halves: We split
nums
intoleft_half = [1, 2]
andright_half = [3, 4]
. -
Generate all possible sums of both halves: We use the
getSubSeqSum
method.For the
left_half
:Using index 0, with currentSum 0 -> left {0} Include number at index 0 -> left {0, 1} Using index 1, with currentSum 1 -> left {0, 1} Include number at index 1 -> left {0, 1, 3} Repeat without including number 1 -> left {0, 1, 3, 2} Include 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
:Using index 0, with currentSum 0 -> right {0} Include number at index 0 -> right {0, 3} Using index 1, with currentSum 3 -> right {0, 3} Include number at index 1 -> right {0, 3, 7} Repeat without including number 3 -> right {0, 3, 7, 4} Include number at index 1 -> right {0, 3, 7, 4, 7}
So the possible sums for
right_half
are{0, 3, 4, 7}
. -
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 theleft_half
sums.left_sum = 0, remaining = goal - 0 = 6, closest in right is 7 (absolute difference 1) left_sum = 1, remaining = goal - 1 = 5, closest in right is 4 or 7 (absolute differences 1 and 2) left_sum = 2, remaining = goal - 2 = 4, closest in right is 4 (absolute difference 0) left_sum = 3, remaining = goal - 3 = 3, closest in right is 3 (absolute difference 0)
-
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
of2
andright_sum
of4
orleft_sum
of3
andright_sum
of3
. Hence the minimum absolute difference between any subsequence sum and thegoal
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
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 are2^n
subsets possible for an array withn
elements, resulting in a time complexity ofO(2^n)
for creating all subsets for an array. - Since
getSubSeqSum
is first called with half the size of the input arrayO(2^(n/2))
, and then with the remaining halfO(2^(n/2))
, the total time for the subset sum generation steps for both halves isO(2*(2^(n/2)))
which simplifies toO(2^(n/2 + 1))
. - The set
right
is sorted afterwards which has at most2^(n/2)
elements leading to a sort time complexity ofO(2^(n/2) * log(2^(n/2)))
which simplifies toO((n/2) * 2^(n/2))
. - Next, the for loop iterates over all elements of
left
, and for each element, a binary search is performed onright
usingbisect_left
. This gives a time complexity ofO(2^(n/2) * log(2^(n/2)))
for the loop which simplifies toO((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))
forleft
andO(2^(n/2))
forright
sets separately. - The recursion call stack of the function
getSubSeqSum
will go to a maximum depth ofn/2
in both halves, so the space used by the call stack isO(n/2 + n/2)
which simplifies toO(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.
Which of the following array represent a max heap?
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!