Facebook Pixel

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 length n where baseCosts[i] is the price of the i-th ice cream base
  • toppingCosts: An array of length m where toppingCosts[i] is the price of one unit of the i-th topping
  • target: 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.

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

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:

  1. Generate all possible topping subset sums (where each topping appears 0 or 1 times) - this is our arr
  2. For each base cost x
  3. For each possible first subset sum y from arr
  4. Use binary search to find the best second subset sum from arr that makes x + 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 target
  • ans = 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 subsets
  • O(2^m * log(2^m)) to sort the array
  • O(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 Evaluator

Example 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 find arr[j] closest to 10 - 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 find arr[j] closest to 10 - 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 find arr[j] closest to 10 - 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 find arr[j] closest to 10 - 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 find arr[j] closest to 10 - 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 with y effectively allows selecting the same combination twice), there are 3^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 costs
    • 3^m values in arr for variable y
    • For each combination, bisect_left performs binary search in O(log(3^m)) time
    • Checking at most 2 positions (indices i and i-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 is O(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]).

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

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

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

Load More