1774. Closest Dessert Cost
Problem Description
In this problem, the aim is to prepare a dessert with a cost as close to the target as possible. There is a selection of n
ice cream base flavors, each with its own price, and m
types of toppings, each topping also with its own price. The conditions for making a dessert are as follows:
- Exactly one ice cream base must be chosen.
- Toppings are optional, but if used, one can choose one or more types, including up to two of each type.
- The goal is to select the combination of one base and any assortment of toppings so that the total cost is as close to the target as possible. If multiple combinations result in the same closest cost, the cheaper one is preferred.
Given are:
baseCosts
, an array representing the cost of each ice cream base flavor.toppingCosts
, an array representing the cost of each type of topping.target
, an integer that represents the desired cost for the dessert.
The task is to find the closest possible cost of the dessert to the target
price.
Flowchart Walkthrough
Let's analyze the problem using the Flowchart to arrive at the right algorithmic pattern for LeetCode problem 1774. Closest Dessert Cost:
-
Is it a graph?
- No: The problem does not involve nodes and edges as in graph problems.
-
Need to solve for kth smallest/largest?
- No: The problem is not about finding the kth smallest or largest element.
-
Involves Linked Lists?
- No: The problem does not involve linked lists.
-
Does the problem have small constraints?
- Yes: The executable permutations and combinations due to the relatively small number of total topping options fit within small constraints.
-
Brute force / Backtracking?
- Yes: Brute force or backtracking is necessary to explore all possible combinations of base costs and toppings to find the closest cost to the target.
Conclusion: The flowchart recommends employing a backtracking approach to explore all possible combinations of dessert costs to identify the closest to the target cost. This is because we need to manage a variety of combinations under small constraints, making backtracking an optimal decision.
Intuition
The solution to this problem can be found by systematic trial and error, using a method known as Depth-First Search (DFS) to explore all possible combinations of toppings, while always keeping track of the running total cost. Here are the steps we take to find the solution:
-
Use DFS to generate all possible sums of the topping costs. Since we can have zero, one, or two of each topping, by the time we traverse through all toppings with DFS, we would have considered every possible combination of topping costs.
-
Sort the array of topping combinations. Sorting will be helpful for finding the closest sums to the target after combining with a base cost.
-
Iterate through each of the base flavors and combine it with every topping combination.
-
Utilize binary search (
bisect_left
) to quickly find the closest topping sum in the sorted array that, when added to the current base cost, is closest to the target. Binary search is more efficient than iterating over each sum since it can zero in on the right range quickly. -
Check both the nearest greater or equal and the immediately smaller sums to handle cases where the closest sum could be less than the target.
-
Compare the total cost (sum of base and toppings) to the target, and if the absolute difference is lower than the difference of any previous combination (or if it's the same but the total cost is lower), update the answer accordingly.
This approach effectively evaluates every feasible combination of toppings and bases, while making use of efficient algorithms like sorting and binary search to minimize the amount of calculations required.
Learn more about Dynamic Programming and Backtracking patterns.
Solution Approach
The implementation of the solution uses recursive depth-first search (DFS), sorting, binary search, and some careful decision logic to evaluate all possible topping combinations and then find the closest cost to the target.
Let's break down the code:
- Depth-First Search (DFS):
def dfs(i, t):
if i >= len(toppingCosts):
arr.append(t)
return
dfs(i + 1, t)
dfs(i + 1, t + toppingCosts[i])
- We define a recursive function
dfs
, which takes an indexi
(indicating the current topping being considered) and a running totalt
of topping costs. - The base case is when
i
is equal to the length oftoppingCosts
array, meaning we have considered all toppings. At this point, we append the total to thearr
list. - We make recursive calls to
dfs
, once without adding the current topping and once with adding the current topping's cost.
- Recursive Calculation of Topping Combinations:
arr = [] dfs(0, 0) arr.sort()
- We initialize an empty list
arr
to store all possible sums of toppings. - The execution of DFS begins with the first topping (index
0
) and a total cost of0
. - Once all combinations are calculated, we sort
arr
, which will allow us to perform efficient binary searches.
- Iterating Over Base Costs and Topping Combinations:
- We iterate over every base cost and topping sum to cover all possible dessert costs.
- Binary Search with
bisect_left
:
for x in baseCosts: for y in arr: i = bisect_left(arr, target - x - y)
- For each base cost
x
and topping sumy
, we usebisect_left
from Python's bisect module to find the indexi
of the first element inarr
that is not less thantarget - x - y
. This helps us identify the closest toppings sum to achieve the target when added to the current base and topping sum.
- Evaluating the Closest Sum:
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]
- Since
bisect_left
may return an index pointing to a larger topping sum (when the exact sum is not present), we check both the sum at indexi
andi - 1
to cover the possibility that a smaller sum could be closer to the target. - We calculate the new distance
t
from the target for this combination. - If this distance is less than the smallest distance found so far (
d
), or if it's the same but the cost is lower, we updated
andans
with the new distance and cost.
- Returning the Closest Cost:
- Finally, we return
ans
, which by the end of the loops, holds the cost of the dessert closest to the target.
The combination of DFS to generate all sums and binary search to quickly find the nearest sums leads to a solution that is both comprehensive and efficient.
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 illustrate the solution approach with a small example.
Suppose we have the following:
baseCosts
= [1, 3]toppingCosts
= [1, 2]target
= 5
Step 1: Depth-First Search for Topping Combinations
We start by generating all possible topping sums:
- Starting with a total of '0', we can either add '0', '1', or '2' of the first topping. This results in possible sums of 0, 1 and 2 for the first topping.
- For each of these sums, we again can add '0', '1', or '2' of the second topping, resulting in sums of 0, 2, 4 when combined with the first topping. So for each sum coming from the first topping, we again get three possibilities.
The resulting sums are: 0
, 1
, 2
, 2
, 3
, 4
, 4
, 5
, and 6
. After removing duplicates and sorting, we have arr
= [0, 1, 2, 3, 4, 5, 6].
Step 2: Iterate over Base Costs and Topping Combinations
We now start with the first base cost, 1
, and iterate through arr
.
Step 3: Binary Search with bisect_left
When looking for the closest sum in arr
to the target - base cost
, we are effectively searching for target - 1 = 4
. The bisect_left
function will give us the index of the first topping sum in arr
that is not less than 4
, which is 4
itself, at index 4
.
Step 4: Evaluating the Closest Sum
We then check the sum at the found index 4
and the sum before it. In arr
these are 4
and 3
. The total cost with the base of 1
would then be 5
and 4
. Since both are equally close to the target of 5
, but 4
is less expensive, we would prefer the total cost of 4
.
We perform the same operations for the second base cost, 3
, looking for target - 3 = 2
. bisect_left
will result in the index of 2
, which points to the sum of 2
exactly in arr
. Checking the sums 2
and 1
(from index 2
and 1
, respectively) gives us total costs of 5
and 4
, again both exactly 1 unit away from the target 5
. Since we already have 4
as our current best, we do not need to update our answer.
Step 5: Returning the Closest Cost
Finally, after considering all base costs, the closest possible cost is 4
, which is the cheapest dessert cost closest to the target of 5
.
Using DFS, we were able to consider all the topping combinations, and with binary search, we quickly identified the closest totals to the target. In the end, we have efficiently arrived at the best possible solution for the problem with a total cost of '4'.
Solution Implementation
1from bisect import bisect_left
2from math import inf
3from typing import List
4
5class Solution:
6 def closestCost(self, base_costs: List[int], topping_costs: List[int], target: int) -> int:
7 # Helper function to perform depth-first search and accumulate costs of toppings
8 def dfs(index, total_cost):
9 # If we have considered all toppings, add the current total_cost to the list
10 if index >= len(topping_costs):
11 possible_toppings_costs.append(total_cost)
12 return
13 # Skip current topping
14 dfs(index + 1, total_cost)
15 # Include current topping once
16 dfs(index + 1, total_cost + topping_costs[index])
17
18 # List to store all possible combinations of toppings' costs
19 possible_toppings_costs = []
20 # Start depth-first search to fill possible_toppings_costs
21 dfs(0, 0)
22 # Sort the list of possible toppings costs for binary search
23 possible_toppings_costs.sort()
24
25 # Initialize variables to keep track of the difference and the closest cost
26 closest_difference = inf
27 closest_cost = inf
28 # Iterate through all base costs
29 for base_cost in base_costs:
30 # Iterate through all possible toppings costs
31 for toppings_cost in possible_toppings_costs:
32 # Calculate the combined cost of base and toppings
33 current_cost = base_cost + toppings_cost
34 # Find the index of the element in the toppings_costs array
35 # that would make the combined cost as close as possible to the target
36 index = bisect_left(possible_toppings_costs, target - current_cost)
37 # Check both the current and the previous index in the array, if possible
38 for check_index in (index, index - 1):
39 if 0 <= check_index < len(possible_toppings_costs):
40 # Calculate the total cost
41 total_cost = current_cost + possible_toppings_costs[check_index]
42 # Calculate the absolute difference from the target
43 difference = abs(total_cost - target)
44 # Update the closest if this is the smallest difference found
45 # or if the difference is the same but the total_cost is smaller
46 if closest_difference > difference or (closest_difference == difference and closest_cost > total_cost):
47 closest_difference = difference
48 closest_cost = total_cost
49
50 # Return the closest cost to the target cost
51 return closest_cost
52
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5class Solution {
6 private List<Integer> toppingCombinations = new ArrayList<>(); // List to store all possible topping combinations
7 private int[] toppings; // Array to store the toppings
8 private final int INF = 1 << 30; // Large number used to for initial values
9
10 // Method to find the closest cost to the target by choosing from base and topping costs
11 // baseCosts: Array of base pizza costs
12 // toppingCosts: Array of topping costs
13 // target: Target cost to achieve
14 public int closestCost(int[] baseCosts, int[] toppingCosts, int target) {
15 toppings = toppingCosts;
16 findAllToppingCombinations(0, 0); // Calls the function to find all combinations of toppings
17 Collections.sort(toppingCombinations); // Sorts the topping combinations for binary search
18
19 int minDifference = INF, closestTotalCost = INF; // Initializing variables to track the closest cost
20 for (int baseCost : baseCosts) { // Loop through each base cost
21 for (int toppingCombinationCost : toppingCombinations) { // Loop through each topping combination
22 int position = binarySearchClosestPosition(target - baseCost - toppingCombinationCost); // Binary search for closest topping combination
23 for (int idx : new int[] {position, position - 1}) { // Check the current and previous position
24 if (idx >= 0 && idx < toppingCombinations.size()) {
25 int currentTotalCost = baseCost + toppingCombinationCost + toppingCombinations.get(idx);
26 int currentDifference = Math.abs(currentTotalCost - target);
27 if (minDifference > currentDifference || (minDifference == currentDifference && closestTotalCost > currentTotalCost)) {
28 minDifference = currentDifference; // Update minimum difference if a closer cost is found
29 closestTotalCost = currentTotalCost; // Update the closest cost
30 }
31 }
32 }
33 }
34 }
35 return closestTotalCost; // Return the cost closest to target, as obtained
36 }
37
38 // Method to perform a binary search to find the closest position in the list
39 private int binarySearchClosestPosition(int x) {
40 int left = 0, right = toppingCombinations.size();
41 while (left < right) {
42 int mid = (left + right) >> 1; // Calculate mid-point for the binary search
43 if (toppingCombinations.get(mid) >= x) {
44 right = mid; // Narrow the search space to the left half
45 } else {
46 left = mid + 1; // Narrow the search space to the right half
47 }
48 }
49 return left; // Return the position of closest value that is >= x
50 }
51
52 // Recursive method to find all possible combinations of toppings
53 private void findAllToppingCombinations(int index, int total) {
54 if (index >= toppings.length) { // Base case: if all toppings have been considered
55 toppingCombinations.add(total); // Add the total to the list of topping combinations
56 return;
57 }
58 // Recursive case: choose the current topping 0 or more times (up to 2 times)
59 findAllToppingCombinations(index + 1, total); // Skip the current topping
60 findAllToppingCombinations(index + 1, total + toppings[index]); // Add the current topping once
61 findAllToppingCombinations(index + 1, total + 2 * toppings[index]); // Add the current topping twice
62 }
63}
64
1class Solution {
2public:
3 int closestCost(vector<int>& baseCosts, vector<int>& toppingCosts, int target) {
4 vector<int> possibleToppingCombinations;
5 // Recursive function to explore all topping combinations
6 function<void(int, int)> exploreToppings = [&](int index, int totalCost) {
7 // If we've considered all toppings, add this total to the possible combinations
8 if (index >= toppingCosts.size()) {
9 possibleToppingCombinations.push_back(totalCost);
10 return;
11 }
12 // Recursive cases: do not add the current topping, add it once, and add it twice
13 exploreToppings(index + 1, totalCost);
14 exploreToppings(index + 1, totalCost + toppingCosts[index]);
15 exploreToppings(index + 1, totalCost + 2 * toppingCosts[index]);
16 };
17
18 // Starting the recursive function with the initial index and cost
19 exploreToppings(0, 0);
20
21 // Sort possible topping combinations for binary searching
22 sort(possibleToppingCombinations.begin(), possibleToppingCombinations.end());
23
24 int closestDifference = INT_MAX;
25 int closestCost = INT_MAX;
26
27 // Iterate over each base cost
28 for (int base : baseCosts) {
29 for (int toppingCombo : possibleToppingCombinations) {
30 // Look for a topping combination that, along with this base, gets closest to the target cost
31 auto it = lower_bound(possibleToppingCombinations.begin(), possibleToppingCombinations.end(), target - base - toppingCombo);
32 for (int k = -1; k <= 1; k++) { // Check both the lower bound and the element before it
33 int idx = distance(possibleToppingCombinations.begin(), it) + k;
34 if (idx >= 0 && idx < possibleToppingCombinations.size()) {
35 int currentCost = base + toppingCombo + possibleToppingCombinations[idx];
36 int currentDiff = abs(currentCost - target);
37 // Update closest cost if a closer or equally close but cheaper cost is found
38 if (closestDifference > currentDiff || (closestDifference == currentDiff && closestCost > currentCost)) {
39 closestDifference = currentDiff;
40 closestCost = currentCost;
41 }
42 }
43 }
44 }
45 }
46 return closestCost;
47 }
48};
49
1// Define the types for better clarity and maintainability.
2type ToppingsVector = number[];
3type LowerBoundFunction = (vec: number[], target: number) => number;
4
5// Recursive function to explore all topping combinations
6const exploreToppings = (toppingCosts: ToppingsVector, index: number, totalCost: number, possibleToppingCombinations: ToppingsVector): void => {
7 // If we've considered all toppings, add this total to the possible combinations
8 if (index >= toppingCosts.length) {
9 possibleToppingCombinations.push(totalCost);
10 return;
11 }
12 // Recursive cases: do not add the current topping, add it once, and add it twice
13 exploreToppings(toppingCosts, index + 1, totalCost, possibleToppingCombinations);
14 exploreToppings(toppingCosts, index + 1, totalCost + toppingCosts[index], possibleToppingCombinations);
15 exploreToppings(toppingCosts, index + 1, totalCost + 2 * toppingCosts[index], possibleToppingCombinations);
16};
17
18// Function to find a close lower bound for a value in a sorted array
19const lowerBound: LowerBoundFunction = (vec, target) => {
20 let left = 0;
21 let right = vec.length;
22
23 while (left < right) {
24 let mid = Math.floor((left + right) / 2);
25 if (vec[mid] < target) left = mid + 1;
26 else right = mid;
27 }
28 return right;
29};
30
31// Main function to compute the closest cost
32const closestCost = (baseCosts: ToppingsVector, toppingCosts: ToppingsVector, target: number): number => {
33 const possibleToppingCombinations: ToppingsVector = [];
34
35 // Generate all possible topping combinations
36 exploreToppings(toppingCosts, 0, 0, possibleToppingCombinations);
37
38 // Sort possible topping combinations for binary searching
39 possibleToppingCombinations.sort((a, b) => a - b);
40
41 let closestDifference = Number.MAX_SAFE_INTEGER;
42 let closestCostValue = Number.MAX_SAFE_INTEGER;
43
44 // Iterate over each base cost
45 baseCosts.forEach(base => {
46 // Look for a topping combination that, along with this base, gets closest to the target cost
47 possibleToppingCombinations.forEach(toppingCombo => {
48 const index = lowerBound(possibleToppingCombinations, target - base - toppingCombo);
49 for (let k = -1; k <= 1; k++) { // Check both the lower bound and the element before it
50 const idx = index + k;
51 if (idx >= 0 && idx < possibleToppingCombinations.length) {
52 const currentCost = base + toppingCombo + possibleToppingCombinations[idx];
53 const currentDiff = Math.abs(currentCost - target);
54 // Update closest cost if a closer or equally close but cheaper cost is found
55 if (closestDifference > currentDiff || (closestDifference === currentDiff && closestCostValue > currentCost)) {
56 closestDifference = currentDiff;
57 closestCostValue = currentCost;
58 }
59 }
60 }
61 });
62 });
63 return closestCostValue;
64};
65
Time and Space Complexity
The given Python code aims to find the closest cost to a target by choosing a base cost and adding zero or more topping costs. The approach taken here is a mix of depth-first search (DFS) for generating all possible topping combinations and binary search for finding the closest sum to the target.
Time Complexity:
- The
dfs
function is called to generate all possible combinations of topping costs, this results in a DFS tree where each node can either include the current topping or not. The time complexity of this part isO(2^T)
whereT
is the number of topping types, since each topping can be used at most once, leading to2^T
combinations. - Sorting the array
arr
has a time complexity ofO(M log M)
whereM
is the number of combinations generated by the DFS, which is also2^T
. - The outer loop iterating through the base costs runs
B
times whereB
is the number of base costs. - Inside the outer loop, a binary search is performed using
bisect_left
. This has a time complexity ofO(log M)
for each of the base costs. - The inner loop runs a constant number of times (2 times, for indices
i
andi-1
).
Combining these factors, the overall time complexity is O(B * (2^T + M log M))
where B
is the number of base costs and M = 2^T
is the number of combinations from the toppings.
In the worst-case scenario, this simplifies to O(B * 2^T + B * T * 2^T)
taking into account both the DFS and the sorting.
Space Complexity:
- The space complexity is driven by the storage required for the
arr
which contains all possible topping combinations, soO(M)
whereM
is the number of combinations, which equals2^T
. - Additionally, there is a recursive stack space due to the
dfs
call which could go as deep asT
levels, thus the space complexity for the call stack isO(T)
.
This means the overall space complexity is O(2^T + T)
, which simplifies to O(2^T)
since 2^T
is expected to be the dominant term as compared to the linear term T
.
Note: inf
is a constant imported from the math
module representing infinity used for comparison, and does not impact the computational complexity.
Learn more about how to find time and space complexity quickly using problem constraints.
How many times is a tree node visited in a depth first search?
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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!