Closest Dessert Cost
MedPrompt
You would like to make dessert and are preparing to buy the ingredients. You have n ice cream base flavors and m types of toppings to choose from. You must follow these rules when making your dessert:
- There must be exactly one ice cream base.
- You can add one or more types of topping or have no toppings at all.
- There are at most two of each type of topping.
You are given three inputs:
baseCosts, an integer array of lengthn, where eachbaseCosts[i]represents the price of theithice cream base flavor.toppingCosts, an integer array of lengthm, where eachtoppingCosts[i]is the price of one of theithtopping.target, an integer representing your target price for dessert.
You want to make a dessert with a total cost as close to target as possible.
Return the closest possible cost of the dessert to target. If there are multiple, return the lower one.
Example 1:
Input: baseCosts = [1,7], toppingCosts = [3,4], target = 10
Output: 10
Explanation: Consider the following combination (all 0-indexed):
- Choose base 1: cost 7
- Take 1 of topping 0: cost 1 x 3 = 3
- Take 0 of topping 1: cost 0 x 4 = 0
Total: 7 + 3 + 0 = 10.Example 2:
Input: baseCosts = [2,3], toppingCosts = [4,5,100], target = 18
Output: 17
Explanation: Consider the following combination (all 0-indexed):
- Choose base 1: cost 3
- Take 1 of topping 0: cost 1 x 4 = 4
- Take 2 of topping 1: cost 2 x 5 = 10
- Take 0 of topping 2: cost 0 x 100 = 0
Total: 3 + 4 + 10 + 0 = 17. You cannot make a dessert with a total cost of 18.Example 3:
Input: baseCosts = [3,10], toppingCosts = [2,5], target = 9
Output: 8
Explanation: It is possible to make desserts with cost 8 and 10. Return 8 as it is the lower cost.
Constraints:
n == baseCosts.lengthm == toppingCosts.length1 <= n, m <= 101 <= baseCosts[i], toppingCosts[i] <= 1041 <= target <= 104
Approaches
2 approaches with complexity analysis and trade-offs.
This approach systematically explores all possible dessert combinations using a recursive depth-first search (DFS). For each of the n base flavors, we recursively try every possible topping combination. For each of the m toppings, there are three choices: take zero, one, or two. This creates a search space of 3^m topping combinations for each base.
Algorithm
- Initialize a global variable
resultto hold the closest cost found so far. A good initial value is the cost of the first base flavor. - Iterate through each
baseCostin thebaseCostsarray. - For each
baseCost, call a recursive helper function,dfs(toppingCosts, index, currentCost). - The
dfsfunction explores the choices for toppings starting fromindex. - In each call to
dfs, first check if thecurrentCostis a better answer than the currentresult. A cost is "better" if it's closer to thetarget, or if it's equally close but lower in value. - The recursion has a base case: when
indexequals the number of toppings, we have considered all toppings and the recursion for that path stops. - As an optimization (pruning), if the
currentCostalready exceeds thetargetand is worse than the current best answerresult, we can stop exploring that path, as adding more (positive cost) toppings will only make the cost higher and the result worse. - The recursive step involves making three calls for the topping at
index:dfs(toppingCosts, index + 1, currentCost): Skip the current topping.dfs(toppingCosts, index + 1, currentCost + toppingCosts[index]): Add one of the current topping.dfs(toppingCosts, index + 1, currentCost + 2 * toppingCosts[index]): Add two of the current topping.
- After the initial loop over base costs completes,
resultwill contain the final answer.
Walkthrough
We initialize a variable, result, to hold the closest cost found so far. A good initial value is the cost of the first base flavor. We then iterate through each baseCost in the baseCosts array. For each baseCost, we start a recursive function, dfs(toppingCosts, index, currentCost), which explores the choices for toppings starting from index. In each call to dfs, we first check if the currentCost is a better answer than our current result. A cost is "better" if it's closer to the target, or if it's equally close but lower in value. The recursion has a base case for when index equals the number of toppings. As an optimization (pruning), if the currentCost already exceeds the target and is worse than the current best answer result, we can stop exploring that path. The recursive step involves making three calls for the topping at index: one for skipping the topping, one for adding it once, and one for adding it twice. After the initial loop over base costs completes, result will contain the final answer.
class Solution { int result; int target; public int closestCost(int[] baseCosts, int[] toppingCosts, int target) { this.result = baseCosts[0]; this.target = target; for (int base : baseCosts) { dfs(toppingCosts, 0, base); } return result; } private void dfs(int[] toppingCosts, int index, int currentCost) { int currentDiff = Math.abs(currentCost - target); int resultDiff = Math.abs(result - target); if (currentDiff < resultDiff || (currentDiff == resultDiff && currentCost < result)) { result = currentCost; } // Base case and pruning if (index == toppingCosts.length || currentCost > target && currentCost > result) { return; } // Recursive calls for the next topping // Choice 1: 0 of current topping dfs(toppingCosts, index + 1, currentCost); // Choice 2: 1 of current topping dfs(toppingCosts, index + 1, currentCost + toppingCosts[index]); // Choice 3: 2 of current topping dfs(toppingCosts, index + 1, currentCost + 2 * toppingCosts[index]); }}Complexity
Time
O(n * 3^m), where `n` is the number of base flavors and `m` is the number of toppings. For each of the `n` base costs, we explore a ternary tree of depth `m`.
Space
O(m), where `m` is the number of toppings. This is the maximum depth of the recursion stack.
Trade-offs
Pros
Conceptually straightforward and follows the problem statement directly.
Low space complexity.
Cons
Exponential time complexity, which is only feasible because the constraints on
mare very small.Can be slower than dynamic programming for certain inputs due to recursion overhead.
This approach uses dynamic programming to determine all possible costs that can be achieved. It's a variation of the knapsack problem. We build up a set of achievable costs by iteratively adding the toppings. This avoids recursion overhead and can be more efficient for the given constraints.
Algorithm
- Define a
maxCostlimit (e.g., 30000), which is a safe upper bound for the answer. - Create a boolean array
dpof sizemaxCost + 1to store achievable costs. - Initialize the
dparray by settingdp[base] = truefor eachbaseinbaseCosts. - For each
tCostintoppingCosts:- To simulate adding the topping at most twice, run the following loop twice:
- For
cfrommaxCostdown totCost:dp[c] = dp[c] || dp[c - tCost]. The backward iteration ensures each unit of the topping is considered at most once per pass.
- After populating the
dparray, iterate fromc = 1tomaxCost. - Initialize
resultandminDiffto track the best answer. - If
dp[c]is true, calculate its difference fromtargetand updateresultandminDiffif it's a better match (smaller difference, or same difference with a lower cost). - Return
result.
Walkthrough
First, we determine the set of all possible total costs using a boolean array, possibleCosts, where possibleCosts[c] is true if a dessert of cost c can be made. We need to establish a maximum possible cost to size the array; a value like 30000 is a safe upper bound given the problem constraints. We initialize the possibleCosts array by marking the costs of all base flavors as true. Then, for each toppingCost, we update the possibleCosts array. To model taking a topping 0, 1, or 2 times, we can treat it as having two identical items of that topping. We iterate through the possibleCosts array backwards for each of these two items, which correctly updates the achievable costs. After processing all toppings, the possibleCosts array is fully populated. Finally, we iterate through this array to find the cost c that is closest to the target, handling ties by choosing the lower cost.
class Solution { public int closestCost(int[] baseCosts, int[] toppingCosts, int target) { int maxCost = 30000; // A safe upper bound for cost boolean[] possible = new boolean[maxCost + 1]; for (int base : baseCosts) { if (base <= maxCost) { possible[base] = true; } } for (int topping : toppingCosts) { // We can add each topping up to two times. // We run the loop twice to simulate adding one or two toppings. // The backward iteration prevents using the same topping unit multiple times in one go. for (int i = 0; i < 2; i++) { for (int cost = maxCost; cost >= topping; cost--) { if (possible[cost - topping]) { possible[cost] = true; } } } } int result = Integer.MAX_VALUE; int minDiff = Integer.MAX_VALUE; for (int cost = 1; cost <= maxCost; cost++) { if (possible[cost]) { int diff = Math.abs(cost - target); if (diff < minDiff) { minDiff = diff; result = cost; } else if (diff == minDiff) { result = Math.min(result, cost); } } } return result; }}Complexity
Time
O(n + m*C), where `n` is the number of bases, `m` is the number of toppings, and `C` is the maximum cost considered. Initializing takes `O(n)`, the DP update takes `O(m*C)`, and the final search takes `O(C)`.
Space
O(C), where `C` is the maximum cost considered. We need a boolean array of size `C` to store the possible costs.
Trade-offs
Pros
Generally faster than backtracking for the given constraints.
Avoids recursion overhead and potential stack overflow issues with deeper recursion.
Cons
Higher space complexity than the backtracking approach.
Complexity depends on the magnitude of costs (pseudo-polynomial), which could be a drawback if costs were very large.
Solutions
Solution
class Solution {private List<Integer> arr = new ArrayList<>();private int[] ts;private int inf = 1 << 30;public int closestCost(int[] baseCosts, int[] toppingCosts, int target) { ts = toppingCosts; dfs(0, 0); Collections.sort(arr); int d = inf, ans = inf;const closestCost = function ( baseCosts , toppingCosts , target ) { let closestDessertCost = - Infinity ; function dfs ( dessertCost , j ) { const tarCurrDiff = Math . abs ( target - dessertCost ); const tarCloseDiff = Math . abs ( target - closestDessertCost ); if ( tarCurrDiff < tarCloseDiff ) { closestDessertCost = dessertCost ; } else if ( tarCurrDiff === tarCloseDiff && dessertCost < closestDessertCost ) { closestDessertCost = dessertCost ; } if ( dessertCost > target ) return ; if ( j === toppingCosts . length ) return ; for ( let count = 0 ; count <= 2 ; count ++ ) { dfs ( dessertCost + count * toppingCosts [ j ], j + 1 ); } } for ( let i = 0 ; i < baseCosts . length ; i ++ ) { dfs ( baseCosts [ i ], 0 ); } return closestDessertCost ; };class Solution {public: const int inf = INT_MAX; int closestCost(vector<int> &baseCosts, vector<int> &toppingCosts, int target) { vector<int> arr; function<void(int, int)> dfs = [&](int i, int t) { if (i >= toppingCosts.size()) { arr.push_back(t); return; } dfs(i + 1, t); dfs(i + 1, t + toppingCosts[i]); }; dfs(0, 0); sort(arr.begin(), arr.end()); int d = inf, ans = inf;class Solution: # 选择一种冰激淋基料 for x in baseCosts : # 枚举子集和 for y in arr : # 二分查找 i = bisect_left ( arr , target - x - y ) 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 ] return ans def closestCost(self, baseCosts: List[int], toppingCosts: List[int], target: int) -> int: def dfs(i, t): if i >= len(toppingCosts): arr . append(t) return dfs(i + 1, t) dfs(i + 1, t + toppingCosts[i]) arr = [] dfs(0, 0) arr . sort() d = ans = infVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.