1774. Closest Dessert Cost
Problem Description
You are making a dessert and need to calculate the cost closest to a target price. The dessert consists of:
Components:
- Ice cream base: You must choose exactly one base flavor
- Toppings: You can choose zero or more types of toppings, and for each topping type you select, you can add either 1 or 2 of that topping
Inputs:
baseCosts
: An array of lengthn
wherebaseCosts[i]
is the price of the i-th ice cream basetoppingCosts
: An array of lengthm
wheretoppingCosts[i]
is the price of one unit of the i-th toppingtarget
: An integer representing your desired total cost
Rules:
- Must select exactly one ice cream base
- Can select 0, 1, or 2 units of each topping type
- Total cost = cost of base + sum of selected toppings
Goal:
Find the total dessert cost that is closest to the target
price. If there are multiple costs with the same distance from the target (e.g., one is above and one is below by the same amount), return the lower cost.
Example:
If target = 10
, and you can make desserts costing 8 and 12, both are distance 2 from the target. You would return 8 (the lower cost).
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem doesn't involve nodes and edges or graph traversal. We're dealing with selecting items (base and toppings) to reach a target cost.
Need to solve for kth smallest/largest?
- No: We're not looking for the kth smallest or largest element. We need to find the cost closest to a target value.
Involves Linked Lists?
- No: The problem uses arrays for costs, not linked list operations.
Does the problem have small constraints?
-
Yes: The problem has relatively small constraints. We need to explore all possible combinations of:
- 1 base (n choices)
- Each topping can be taken 0, 1, or 2 times (3^m possibilities for m toppings)
This creates a finite search space that can be explored exhaustively.
Brute force / Backtracking?
- Yes: We need to explore all possible combinations of toppings (0, 1, or 2 of each type) combined with each base. Backtracking is perfect for generating all these combinations systematically.
Conclusion: The flowchart suggests using a backtracking approach. We can use backtracking to generate all possible topping combinations (where each topping can appear 0, 1, or 2 times), then combine each of these with every base to find the total cost closest to the target.
Intuition
The key insight is that we need to explore all possible dessert combinations to find the one closest to our target price. Since each topping can be taken 0, 1, or 2 times, we have 3 choices for each topping type. This naturally leads to a backtracking approach.
Think of it like building a decision tree: for each topping, we branch into three possibilities (take 0, 1, or 2). This gives us 3^m
different topping combinations. However, the provided solution takes a clever optimization approach.
Instead of generating all 3^m
combinations (which could be expensive), the solution first generates all possible sums using each topping at most once (0 or 1 times). This gives us 2^m
combinations stored in the arr
array through the DFS function. This is more efficient because 2^m < 3^m
.
But wait - we need to handle the case where toppings can be taken twice! The solution handles this elegantly: since we can take up to 2 of each topping, we can think of it as selecting two (possibly overlapping) subsets of toppings. So for any dessert, the total topping cost can be represented as the sum of two values from our arr
array.
The final approach becomes:
- Generate all possible topping subset sums (where each topping appears 0 or 1 times) - this is our
arr
- For each base cost
x
- For each possible first subset sum
y
fromarr
- Use binary search to find the best second subset sum from
arr
that makesx + y + arr[j]
closest to target
The binary search optimization works because we're looking for arr[j]
such that x + y + arr[j]
is closest to target
, which means we want arr[j]
closest to target - x - y
. By sorting arr
first, we can use binary search to quickly find candidates around this value.
Learn more about Dynamic Programming and Backtracking patterns.
Solution Approach
The implementation uses a combination of backtracking (via DFS) and binary search to efficiently find the optimal dessert cost.
Step 1: Generate all possible topping subset sums
The dfs
function generates all possible sums where each topping is taken 0 or 1 times:
def dfs(i, t):
if i >= len(toppingCosts):
arr.append(t)
return
dfs(i + 1, t) # Don't take topping i
dfs(i + 1, t + toppingCosts[i]) # Take one of topping i
This creates 2^m
different sums stored in arr
. Starting with dfs(0, 0)
, we recursively decide for each topping whether to include it or not, accumulating the sum in parameter t
.
Step 2: Sort the array for binary search
arr.sort()
Sorting enables us to use binary search later to quickly find values close to our target.
Step 3: Try all combinations
Initialize tracking variables:
d = inf
: tracks the minimum distance from targetans = inf
: stores the best cost found
For each base x
and each first topping subset sum y
:
for x in baseCosts: for y in arr:
Step 4: Binary search for the optimal third component
We need to find arr[j]
such that x + y + arr[j]
is closest to target
. This means finding arr[j]
closest to target - x - y
:
i = bisect_left(arr, target - x - y)
bisect_left
returns the insertion point where target - x - y
would go in the sorted array. We check both positions i
and i-1
because:
arr[i]
is the smallest value ≥target - x - y
arr[i-1]
is the largest value <target - x - y
One of these will give us the closest sum.
Step 5: Update the answer
for j in (i, i - 1):
if 0 <= j < len(arr):
t = abs(x + y + arr[j] - target)
if d > t or (d == t and ans > x + y + arr[j]):
d = t
ans = x + y + arr[j]
We update our answer if:
- We found a closer cost to target (smaller distance
t
) - We found an equal distance but lower cost (tiebreaker rule)
The time complexity is O(n * 4^m + m * 2^m * log(2^m))
where:
O(2^m)
to generate all topping subsetsO(2^m * log(2^m))
to sort the arrayO(n * 2^m * 2^m * log(2^m))
=O(n * 4^m * m)
for the triple nested loop with binary search
Since we're using each value in arr
twice (as y
and as arr[j]
), this effectively covers all cases where each topping can be taken 0, 1, or 2 times.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Given:
baseCosts = [1, 7]
toppingCosts = [3, 4]
target = 10
Step 1: Generate all topping subset sums using DFS
We'll trace through the DFS to generate all possible sums where each topping is taken 0 or 1 times:
dfs(0, 0): ├─ dfs(1, 0): # Don't take topping 0 (cost=3) │ ├─ dfs(2, 0): # Don't take topping 1 (cost=4) │ │ └─ arr.append(0) │ └─ dfs(2, 4): # Take topping 1 (cost=4) │ └─ arr.append(4) └─ dfs(1, 3): # Take topping 0 (cost=3) ├─ dfs(2, 3): # Don't take topping 1 (cost=4) │ └─ arr.append(3) └─ dfs(2, 7): # Take topping 1 (cost=4) └─ arr.append(7)
Result: arr = [0, 4, 3, 7]
Step 2: Sort the array
arr = [0, 3, 4, 7]
(sorted)
Step 3: Try all combinations
Initialize: d = inf
, ans = inf
For each base and each first topping subset:
Base = 1:
-
When
y = 0
: Need to findarr[j]
closest to10 - 1 - 0 = 9
- Binary search finds insertion point at index 4 (after 7)
- Check
j = 3
: cost =1 + 0 + 7 = 8
, distance =|8 - 10| = 2
- Update:
d = 2
,ans = 8
-
When
y = 3
: Need to findarr[j]
closest to10 - 1 - 3 = 6
- Binary search finds insertion point at index 3 (between 4 and 7)
- Check
j = 3
: cost =1 + 3 + 7 = 11
, distance =|11 - 10| = 1
- Check
j = 2
: cost =1 + 3 + 4 = 8
, distance =|8 - 10| = 2
- Update:
d = 1
,ans = 11
-
When
y = 4
: Need to findarr[j]
closest to10 - 1 - 4 = 5
- Binary search finds insertion point at index 3
- Check
j = 3
: cost =1 + 4 + 7 = 12
, distance =|12 - 10| = 2
- Check
j = 2
: cost =1 + 4 + 4 = 9
, distance =|9 - 10| = 1
- No update (same distance, but 11 > 9, so we keep ans = 11)
-
When
y = 7
: Need to findarr[j]
closest to10 - 1 - 7 = 2
- Binary search finds insertion point at index 1
- Check
j = 1
: cost =1 + 7 + 3 = 11
, distance =|11 - 10| = 1
- Check
j = 0
: cost =1 + 7 + 0 = 8
, distance =|8 - 10| = 2
- No update (already have distance 1)
Base = 7:
- When
y = 0
: Need to findarr[j]
closest to10 - 7 - 0 = 3
- Binary search finds insertion point at index 1
- Check
j = 1
: cost =7 + 0 + 3 = 10
, distance =|10 - 10| = 0
- Update:
d = 0
,ans = 10
✓
Since we found an exact match (distance = 0), we have our answer: 10
This corresponds to choosing:
- Base with cost 7
- One unit of the first topping (cost 3)
- Zero units of the second topping
- Total: 7 + 3 = 10
Solution Implementation
1from typing import List
2from bisect import bisect_left
3from math import inf
4
5
6class Solution:
7 def closestCost(
8 self, baseCosts: List[int], toppingCosts: List[int], target: int
9 ) -> int:
10 def generate_topping_combinations(index: int, current_cost: int) -> None:
11 """
12 Generate all possible topping combinations using DFS.
13 Each topping can be used 0, 1, or 2 times.
14 """
15 # Base case: processed all toppings
16 if index >= len(toppingCosts):
17 topping_combinations.append(current_cost)
18 return
19
20 # Option 1: Don't add current topping (0 times)
21 generate_topping_combinations(index + 1, current_cost)
22
23 # Option 2: Add current topping once
24 generate_topping_combinations(index + 1, current_cost + toppingCosts[index])
25
26 # Option 3: Add current topping twice
27 generate_topping_combinations(index + 1, current_cost + 2 * toppingCosts[index])
28
29 # Generate all possible topping combination costs
30 topping_combinations = []
31 generate_topping_combinations(0, 0)
32 topping_combinations.sort()
33
34 # Initialize variables to track the best result
35 min_difference = inf
36 closest_cost = inf
37
38 # Try each base cost
39 for base_cost in baseCosts:
40 # Try each topping combination
41 for topping_sum in topping_combinations:
42 # Calculate remaining cost needed to reach target
43 remaining = target - base_cost - topping_sum
44
45 # Binary search for the closest topping combination to the remaining cost
46 insertion_point = bisect_left(topping_combinations, remaining)
47
48 # Check both the insertion point and the element before it
49 for candidate_index in (insertion_point, insertion_point - 1):
50 if 0 <= candidate_index < len(topping_combinations):
51 # Calculate total cost with this combination
52 total_cost = base_cost + topping_sum + topping_combinations[candidate_index]
53 current_difference = abs(total_cost - target)
54
55 # Update best result if this is closer to target
56 # or equally close but with smaller cost
57 if (current_difference < min_difference or
58 (current_difference == min_difference and total_cost < closest_cost)):
59 min_difference = current_difference
60 closest_cost = total_cost
61
62 return closest_cost
63
1class Solution {
2 // List to store all possible topping combinations
3 private List<Integer> toppingCombinations = new ArrayList<>();
4 // Array to store topping costs
5 private int[] toppingCosts;
6 // Maximum value for comparison
7 private static final int INFINITY = 1 << 30;
8
9 /**
10 * Find the closest possible cost to target using one base and up to two of each topping
11 * @param baseCosts array of base ice cream costs
12 * @param toppingCosts array of topping costs
13 * @param target the target cost we want to achieve
14 * @return the closest possible total cost to target
15 */
16 public int closestCost(int[] baseCosts, int[] toppingCosts, int target) {
17 this.toppingCosts = toppingCosts;
18
19 // Generate all possible topping combinations (0, 1, or 2 of each topping)
20 generateToppingCombinations(0, 0);
21
22 // Sort combinations for binary search
23 Collections.sort(toppingCombinations);
24
25 int minDifference = INFINITY;
26 int closestTotalCost = INFINITY;
27
28 // Try each base cost
29 for (int baseCost : baseCosts) {
30 // Try each topping combination
31 for (int toppingSum : toppingCombinations) {
32 // Find the remaining cost needed to reach target
33 int remainingTarget = target - baseCost - toppingSum;
34
35 // Binary search for closest topping combination to remaining target
36 int index = binarySearchClosest(remainingTarget);
37
38 // Check both the found index and the previous one for closest match
39 for (int candidateIndex : new int[] {index, index - 1}) {
40 if (candidateIndex >= 0 && candidateIndex < toppingCombinations.size()) {
41 int totalCost = baseCost + toppingSum + toppingCombinations.get(candidateIndex);
42 int difference = Math.abs(totalCost - target);
43
44 // Update result if this combination is closer to target
45 // or has same difference but smaller total cost
46 if (difference < minDifference ||
47 (difference == minDifference && totalCost < closestTotalCost)) {
48 minDifference = difference;
49 closestTotalCost = totalCost;
50 }
51 }
52 }
53 }
54 }
55
56 return closestTotalCost;
57 }
58
59 /**
60 * Binary search to find the index of the smallest element >= target value
61 * @param targetValue the value to search for
62 * @return index of the smallest element >= targetValue
63 */
64 private int binarySearchClosest(int targetValue) {
65 int left = 0;
66 int right = toppingCombinations.size();
67
68 while (left < right) {
69 int mid = (left + right) >> 1;
70 if (toppingCombinations.get(mid) >= targetValue) {
71 right = mid;
72 } else {
73 left = mid + 1;
74 }
75 }
76
77 return left;
78 }
79
80 /**
81 * Generate all possible topping combinations using DFS
82 * Each topping can be used 0, 1, or 2 times
83 * @param index current topping index
84 * @param currentSum current sum of selected toppings
85 */
86 private void generateToppingCombinations(int index, int currentSum) {
87 // Base case: processed all toppings
88 if (index >= toppingCosts.length) {
89 toppingCombinations.add(currentSum);
90 return;
91 }
92
93 // Option 1: Don't use current topping
94 generateToppingCombinations(index + 1, currentSum);
95
96 // Option 2: Use current topping once
97 generateToppingCombinations(index + 1, currentSum + toppingCosts[index]);
98
99 // Note: The original code only considers 0 or 1 of each topping
100 // For the complete problem (0, 1, or 2), add:
101 // generateToppingCombinations(index + 1, currentSum + 2 * toppingCosts[index]);
102 }
103}
104
1class Solution {
2public:
3 const int INF = INT_MAX;
4
5 int closestCost(vector<int>& baseCosts, vector<int>& toppingCosts, int target) {
6 // Store all possible topping combination costs
7 vector<int> toppingCombinations;
8
9 // Generate all possible topping combinations using DFS
10 // Each topping can be chosen 0, 1, or 2 times
11 function<void(int, int)> generateToppingCombinations = [&](int toppingIndex, int currentCost) {
12 // Base case: processed all toppings
13 if (toppingIndex >= toppingCosts.size()) {
14 toppingCombinations.push_back(currentCost);
15 return;
16 }
17
18 // Option 1: Don't use current topping (0 times)
19 generateToppingCombinations(toppingIndex + 1, currentCost);
20
21 // Option 2: Use current topping once
22 generateToppingCombinations(toppingIndex + 1, currentCost + toppingCosts[toppingIndex]);
23 };
24
25 // Generate all topping combinations starting from cost 0
26 generateToppingCombinations(0, 0);
27
28 // Sort topping combinations for binary search
29 sort(toppingCombinations.begin(), toppingCombinations.end());
30
31 int minDifference = INF;
32 int closestTotalCost = INF;
33
34 // Try each base cost
35 for (int baseCost : baseCosts) {
36 // Try each topping combination with current base
37 for (int toppingCost : toppingCombinations) {
38 // Find the topping combination that makes total cost closest to target
39 // targetToppingCost = target - baseCost - toppingCost
40 int targetRemainder = target - baseCost - toppingCost;
41
42 // Binary search for closest topping combination to targetRemainder
43 int searchIndex = lower_bound(toppingCombinations.begin(),
44 toppingCombinations.end(),
45 targetRemainder) - toppingCombinations.begin();
46
47 // Check adjacent elements for the closest match
48 for (int checkIndex = searchIndex - 1; checkIndex <= searchIndex; ++checkIndex) {
49 if (checkIndex >= 0 && checkIndex < toppingCombinations.size()) {
50 // Calculate total cost and difference from target
51 int totalCost = baseCost + toppingCost + toppingCombinations[checkIndex];
52 int currentDifference = abs(totalCost - target);
53
54 // Update result if this is closer to target
55 // or if equally close but with smaller total cost
56 if (currentDifference < minDifference ||
57 (currentDifference == minDifference && totalCost < closestTotalCost)) {
58 minDifference = currentDifference;
59 closestTotalCost = totalCost;
60 }
61 }
62 }
63 }
64 }
65
66 return closestTotalCost;
67 }
68};
69
1/**
2 * Finds the closest possible dessert cost to the target price
3 * @param baseCosts - Array of base ice cream costs (must choose exactly one)
4 * @param toppingCosts - Array of topping costs (can choose 0, 1, or 2 of each)
5 * @param target - Target price to get as close as possible to
6 * @returns The closest possible total cost to the target
7 */
8const closestCost = function (baseCosts: number[], toppingCosts: number[], target: number): number {
9 // Track the closest dessert cost found so far
10 let closestDessertCost: number = -Infinity;
11
12 /**
13 * Depth-first search to explore all possible topping combinations
14 * @param currentDessertCost - Current accumulated cost of the dessert
15 * @param toppingIndex - Current index in the toppingCosts array
16 */
17 function dfs(currentDessertCost: number, toppingIndex: number): void {
18 // Calculate differences between target and current/closest costs
19 const targetToCurrentDiff: number = Math.abs(target - currentDessertCost);
20 const targetToClosestDiff: number = Math.abs(target - closestDessertCost);
21
22 // Update closest cost if current is better (closer to target)
23 if (targetToCurrentDiff < targetToClosestDiff) {
24 closestDessertCost = currentDessertCost;
25 }
26 // If equally close to target, prefer the smaller cost
27 else if (targetToCurrentDiff === targetToClosestDiff && currentDessertCost < closestDessertCost) {
28 closestDessertCost = currentDessertCost;
29 }
30
31 // Prune: stop exploring if current cost exceeds target
32 if (currentDessertCost > target) {
33 return;
34 }
35
36 // Base case: all toppings have been considered
37 if (toppingIndex === toppingCosts.length) {
38 return;
39 }
40
41 // Try adding 0, 1, or 2 of the current topping
42 for (let count: number = 0; count <= 2; count++) {
43 dfs(currentDessertCost + count * toppingCosts[toppingIndex], toppingIndex + 1);
44 }
45 }
46
47 // Try each base as the starting point
48 for (let i: number = 0; i < baseCosts.length; i++) {
49 dfs(baseCosts[i], 0);
50 }
51
52 return closestDessertCost;
53};
54
Time and Space Complexity
Time Complexity: O(3^m + n * 4^m * log(3^m))
where n
is the length of baseCosts
and m
is the length of toppingCosts
.
The analysis breaks down as follows:
- The
dfs
function generates all possible topping combinations. Since each topping can be chosen 0, 1, or 2 times (the code shows 0 or 1 time per recursive call, but the outer loop withy
effectively allows selecting the same combination twice), there are3^m
possible combinations. - Generating all combinations takes
O(3^m)
time. - Sorting the array takes
O(3^m * log(3^m))
time. - The main nested loops iterate through:
n
base costs3^m
values inarr
for variabley
- For each combination,
bisect_left
performs binary search inO(log(3^m))
time - Checking at most 2 positions (indices
i
andi-1
)
- Total for the nested loops:
O(n * 3^m * log(3^m))
Since the code iterates through arr
twice (once for y
and once for binary search target), the effective complexity becomes O(n * 9^m * log(3^m))
which simplifies to O(n * 4^m * log(3^m))
considering the dominant terms.
Space Complexity: O(3^m)
where m
is the length of toppingCosts
.
The space usage comes from:
- The
arr
list storing all possible topping combinations:O(3^m)
- The recursion stack depth for DFS:
O(m)
- Since
3^m >> m
, the overall space complexity isO(3^m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Topping Combination Generation
A common mistake is generating topping combinations where each topping can only be used 0 or 1 times, forgetting that the problem allows using up to 2 units of each topping.
Incorrect approach:
def generate_combinations(index, current_cost):
if index >= len(toppingCosts):
combinations.append(current_cost)
return
# Only considering 0 or 1 of each topping
generate_combinations(index + 1, current_cost)
generate_combinations(index + 1, current_cost + toppingCosts[index])
Correct approach:
def generate_combinations(index, current_cost):
if index >= len(toppingCosts):
combinations.append(current_cost)
return
# Consider 0, 1, or 2 of each topping
generate_combinations(index + 1, current_cost)
generate_combinations(index + 1, current_cost + toppingCosts[index])
generate_combinations(index + 1, current_cost + 2 * toppingCosts[index])
2. Exponential Memory Usage Without Optimization
The current solution generates 3^m combinations (where m is the number of toppings), which can cause memory issues for large m. The provided solution actually uses a clever optimization by generating 2^m combinations and using them twice, but developers might miss this optimization.
Memory-intensive approach:
# Generates 3^m combinations directly topping_combinations = [] generate_all_3_way_combinations(0, 0) # This creates 3^m entries
Optimized approach (as in the solution):
# Generate 2^m combinations and reuse them # The double loop (for y in arr, for arr[j]) effectively covers all 3^m cases
3. Incorrect Tie-Breaking Logic
When two costs have the same distance from the target, the problem requires returning the lower cost. A common mistake is to return the higher cost or not handle ties at all.
Incorrect:
if current_difference < min_difference: min_difference = current_difference closest_cost = total_cost # Missing the tie-breaker condition
Correct:
if (current_difference < min_difference or (current_difference == min_difference and total_cost < closest_cost)): min_difference = current_difference closest_cost = total_cost
4. Off-by-One Errors in Binary Search
When using bisect_left
, developers often forget to check both the insertion point and the previous element, missing potentially optimal solutions.
Incorrect:
insertion_point = bisect_left(topping_combinations, remaining)
if insertion_point < len(topping_combinations):
# Only checking one position
total_cost = base_cost + topping_sum + topping_combinations[insertion_point]
Correct:
insertion_point = bisect_left(topping_combinations, remaining)
# Check both positions: the insertion point and the element before it
for candidate_index in (insertion_point, insertion_point - 1):
if 0 <= candidate_index < len(topping_combinations):
total_cost = base_cost + topping_sum + topping_combinations[candidate_index]
5. Not Handling Edge Cases
Forgetting to handle cases where the topping array is empty or when all combinations exceed the target.
Solution: Always initialize tracking variables properly and ensure the algorithm works even when toppingCosts
is empty (the combination array will just contain [0]).
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!