2431. Maximize Total Tastiness of Purchased Fruits 🔒
Problem Description
You have access to a fruit market where each fruit has a specific price and tastiness value. Given two arrays of the same length n
:
price[i]
represents the cost of the i-th fruittastiness[i]
represents how tasty the i-th fruit is
You have a budget of maxAmount
money and maxCoupons
discount coupons to spend.
The goal is to buy fruits to maximize the total tastiness while staying within your budget.
Rules for purchasing:
- Each fruit can be bought at most once (you can't buy multiple of the same fruit)
- For any fruit, you can choose to:
- Buy it at full price: costs
price[i]
- Buy it with a coupon at half price: costs
price[i] // 2
(integer division, rounded down) - Not buy it at all
- Buy it at full price: costs
- Each coupon can only be used once, and you have at most
maxCoupons
coupons total - The total spending cannot exceed
maxAmount
Example scenarios:
- If a fruit costs 10 and you use a coupon, you pay 5
- If a fruit costs 11 and you use a coupon, you pay 5 (rounded down from 5.5)
- You can choose to buy some fruits with coupons, some at full price, and skip others entirely
The problem asks you to return the maximum possible sum of tastiness values you can achieve by strategically selecting which fruits to buy and which ones to apply coupons to, while respecting all constraints.
Intuition
This problem presents us with multiple choices at each step - for every fruit, we need to decide whether to buy it or not, and if we buy it, whether to use a coupon. This decision-making pattern naturally suggests a dynamic programming approach.
Think of it as exploring all possible purchasing strategies. At each fruit, we're at a decision point with three options:
- Skip this fruit entirely
- Buy it at full price (if we have enough money)
- Buy it with a coupon (if we have enough money for half price AND have coupons left)
The key insight is that our decision for the current fruit depends on:
- Which fruit we're considering (position
i
) - How much money we have left (
j
) - How many coupons we have left (
k
)
These three values completely define our "state" at any point in the decision process. Once we know these three things, the best decision from that point forward is always the same - this is the optimal substructure property that makes dynamic programming applicable.
We can visualize this as a tree of decisions. Starting from the first fruit with full budget and all coupons, we branch into different scenarios based on our choices. Many of these branches will lead to the same state (same fruit index, same remaining money, same remaining coupons), which means we're solving the same subproblem multiple times. This overlap in subproblems is why memoization is effective here.
The recursive function dfs(i, j, k)
captures this beautifully - it answers the question: "What's the maximum tastiness I can get starting from fruit i
, with j
money and k
coupons remaining?" By caching the results, we avoid recalculating the same scenarios and efficiently explore all possible purchasing strategies to find the optimal one.
The base case is when we've considered all fruits (i == len(price)
), at which point no more tastiness can be gained, so we return 0.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach using memoization search. Let's break down the implementation:
Function Definition:
We define a recursive function dfs(i, j, k)
that represents:
i
: the index of the current fruit we're consideringj
: the amount of money remainingk
: the number of coupons remaining- Returns: the maximum total tastiness achievable from fruit
i
onwards with the given constraints
Base Case:
if i == len(price):
return 0
When we've considered all fruits (reached the end of the array), no more tastiness can be gained, so we return 0.
Recursive Cases:
For each fruit at position i
, we explore three choices:
-
Skip the current fruit:
ans = dfs(i + 1, j, k)
We move to the next fruit without spending money or coupons, keeping the same budget and coupon count.
-
Buy at full price (if affordable):
if j >= price[i]: ans = max(ans, dfs(i + 1, j - price[i], k) + tastiness[i])
If we have enough money (
j >= price[i]
), we can buy this fruit at full price. We recursively solve for the remaining fruits with reduced money (j - price[i]
) and add the current fruit's tastiness to the result. -
Buy with a coupon (if affordable and coupons available):
if j >= price[i] // 2 and k: ans = max(ans, dfs(i + 1, j - price[i] // 2, k - 1) + tastiness[i])
If we have enough money for half price (
j >= price[i] // 2
) AND have at least one coupon (k > 0
), we can use a coupon. We recursively solve with reduced money (j - price[i] // 2
) and one less coupon (k - 1
), adding the current fruit's tastiness.
Memoization:
The @cache
decorator automatically memoizes the function results. When dfs(i, j, k)
is called with the same parameters again, it returns the cached result instead of recalculating. This prevents redundant computation of overlapping subproblems.
Final Answer:
return dfs(0, maxAmount, maxCoupons)
We start the recursion from fruit 0 with the full budget and all coupons available.
Time Complexity: O(n × maxAmount × maxCoupons)
where n is the number of fruits, as each unique state is computed only once.
Space Complexity: O(n × maxAmount × maxCoupons)
for the memoization cache plus O(n)
for the recursion stack depth.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach:
Input:
price = [10, 20, 20]
tastiness = [5, 8, 8]
maxAmount = 20
maxCoupons = 1
Goal: Find the maximum total tastiness we can achieve.
Let's trace through the recursive calls starting from dfs(0, 20, 1)
:
Step 1: dfs(0, 20, 1) - Considering fruit 0 (price=10, tastiness=5)
- Option 1: Skip fruit 0 →
dfs(1, 20, 1)
- Option 2: Buy at full price (20 ≥ 10) →
dfs(1, 10, 1) + 5
- Option 3: Buy with coupon (20 ≥ 5 and have 1 coupon) →
dfs(1, 15, 0) + 5
Step 2: Explore dfs(1, 20, 1) - Considering fruit 1 (price=20, tastiness=8)
- Option 1: Skip →
dfs(2, 20, 1)
- Option 2: Buy at full price (20 ≥ 20) →
dfs(2, 0, 1) + 8
- Option 3: Buy with coupon (20 ≥ 10 and have 1 coupon) →
dfs(2, 10, 0) + 8
Step 3: Explore dfs(1, 10, 1) - From buying fruit 0 at full price
- Option 1: Skip →
dfs(2, 10, 1)
- Option 2: Can't buy at full price (10 < 20)
- Option 3: Buy with coupon (10 ≥ 10 and have 1 coupon) →
dfs(2, 0, 0) + 8
Step 4: Explore dfs(1, 15, 0) - From buying fruit 0 with coupon
- Option 1: Skip →
dfs(2, 15, 0)
- Option 2: Can't buy at full price (15 < 20)
- Option 3: Can't use coupon (have 0 coupons)
Continue exploring leaf nodes...
When we reach dfs(2, 10, 0)
from the path where we bought fruit 1 with a coupon:
- We can buy fruit 2 with the remaining 10 money (price=20, half price=10)
- But we have no coupons left, so we can only skip it
Backtracking and Computing Results:
Key paths to consider:
- Buy nothing → tastiness = 0
- Buy fruit 0 with coupon (cost 5), then buy fruit 1 with remaining money at full price (can't afford) → tastiness = 5
- Buy fruit 1 with coupon (cost 10), then buy fruit 2 at full price (can't afford with remaining 10) → tastiness = 8
- Buy fruit 0 at full price (cost 10), then buy fruit 1 with coupon (cost 10) → tastiness = 5 + 8 = 13 ✓
The maximum tastiness achieved is 13 by buying fruit 0 at full price and fruit 1 with the coupon.
The memoization ensures that when we encounter the same state (i, j, k)
again (like dfs(2, 10, 0)
might be reached through different paths), we don't recalculate it.
Solution Implementation
1class Solution:
2 def maxTastiness(
3 self, price: List[int], tastiness: List[int], maxAmount: int, maxCoupons: int
4 ) -> int:
5 """
6 Find the maximum total tastiness achievable when buying fruits.
7
8 Args:
9 price: List of fruit prices
10 tastiness: List of fruit tastiness values
11 maxAmount: Maximum amount of money available
12 maxCoupons: Maximum number of 50% discount coupons available
13
14 Returns:
15 Maximum total tastiness achievable
16 """
17
18 @cache
19 def dfs(fruit_index: int, remaining_amount: int, remaining_coupons: int) -> int:
20 """
21 Dynamic programming helper function to calculate maximum tastiness.
22
23 Args:
24 fruit_index: Current index in the fruit arrays
25 remaining_amount: Remaining money available
26 remaining_coupons: Remaining coupons available
27
28 Returns:
29 Maximum tastiness achievable from current state
30 """
31 # Base case: no more fruits to consider
32 if fruit_index == len(price):
33 return 0
34
35 # Option 1: Skip current fruit
36 max_tastiness = dfs(fruit_index + 1, remaining_amount, remaining_coupons)
37
38 # Option 2: Buy current fruit at full price (if affordable)
39 if remaining_amount >= price[fruit_index]:
40 max_tastiness = max(
41 max_tastiness,
42 dfs(fruit_index + 1, remaining_amount - price[fruit_index], remaining_coupons) + tastiness[fruit_index]
43 )
44
45 # Option 3: Buy current fruit with 50% coupon (if affordable and coupons available)
46 half_price = price[fruit_index] // 2
47 if remaining_amount >= half_price and remaining_coupons > 0:
48 max_tastiness = max(
49 max_tastiness,
50 dfs(fruit_index + 1, remaining_amount - half_price, remaining_coupons - 1) + tastiness[fruit_index]
51 )
52
53 return max_tastiness
54
55 # Start DFS from first fruit with full budget and all coupons
56 return dfs(0, maxAmount, maxCoupons)
57
1class Solution {
2 // Memoization table: dp[itemIndex][remainingAmount][remainingCoupons]
3 private int[][][] dp;
4 private int[] price;
5 private int[] tastiness;
6 private int n;
7
8 /**
9 * Finds the maximum tastiness achievable with given budget and coupons.
10 * Each item can be bought at full price or half price (using a coupon).
11 *
12 * @param price Array of item prices
13 * @param tastiness Array of item tastiness values
14 * @param maxAmount Maximum amount of money available
15 * @param maxCoupons Maximum number of coupons available
16 * @return Maximum total tastiness achievable
17 */
18 public int maxTastiness(int[] price, int[] tastiness, int maxAmount, int maxCoupons) {
19 this.n = price.length;
20 this.price = price;
21 this.tastiness = tastiness;
22 // Initialize memoization table with dimensions for all states
23 this.dp = new int[n][maxAmount + 1][maxCoupons + 1];
24
25 // Start DFS from first item with full budget and all coupons
26 return dfs(0, maxAmount, maxCoupons);
27 }
28
29 /**
30 * Recursive DFS with memoization to find maximum tastiness.
31 *
32 * @param itemIndex Current item index being considered
33 * @param remainingAmount Remaining money available
34 * @param remainingCoupons Remaining coupons available
35 * @return Maximum tastiness achievable from current state
36 */
37 private int dfs(int itemIndex, int remainingAmount, int remainingCoupons) {
38 // Base case: no more items to consider
39 if (itemIndex == n) {
40 return 0;
41 }
42
43 // Return memoized result if already computed
44 if (dp[itemIndex][remainingAmount][remainingCoupons] != 0) {
45 return dp[itemIndex][remainingAmount][remainingCoupons];
46 }
47
48 // Option 1: Skip current item
49 int maxTastiness = dfs(itemIndex + 1, remainingAmount, remainingCoupons);
50
51 // Option 2: Buy current item at full price if affordable
52 if (remainingAmount >= price[itemIndex]) {
53 maxTastiness = Math.max(
54 maxTastiness,
55 dfs(itemIndex + 1, remainingAmount - price[itemIndex], remainingCoupons) + tastiness[itemIndex]
56 );
57 }
58
59 // Option 3: Buy current item at half price using a coupon if affordable and coupons available
60 if (remainingAmount >= price[itemIndex] / 2 && remainingCoupons > 0) {
61 maxTastiness = Math.max(
62 maxTastiness,
63 dfs(itemIndex + 1, remainingAmount - price[itemIndex] / 2, remainingCoupons - 1) + tastiness[itemIndex]
64 );
65 }
66
67 // Memoize and return the result
68 dp[itemIndex][remainingAmount][remainingCoupons] = maxTastiness;
69 return maxTastiness;
70 }
71}
72
1class Solution {
2public:
3 int maxTastiness(vector<int>& price, vector<int>& tastiness, int maxAmount, int maxCoupons) {
4 int itemCount = price.size();
5
6 // Initialize memoization table to 0
7 // dp[i][j][k] represents max tastiness starting from item i with j amount and k coupons
8 memset(dp, 0, sizeof(dp));
9
10 // Define recursive function with memoization
11 function<int(int, int, int)> calculateMaxTastiness;
12
13 calculateMaxTastiness = [&](int itemIndex, int remainingAmount, int remainingCoupons) -> int {
14 // Base case: no more items to consider
15 if (itemIndex == itemCount) {
16 return 0;
17 }
18
19 // Return memoized result if already computed
20 if (dp[itemIndex][remainingAmount][remainingCoupons] != 0) {
21 return dp[itemIndex][remainingAmount][remainingCoupons];
22 }
23
24 // Option 1: Skip current item
25 int maxValue = calculateMaxTastiness(itemIndex + 1, remainingAmount, remainingCoupons);
26
27 // Option 2: Buy item at full price if we have enough money
28 if (remainingAmount >= price[itemIndex]) {
29 maxValue = max(maxValue,
30 calculateMaxTastiness(itemIndex + 1,
31 remainingAmount - price[itemIndex],
32 remainingCoupons) + tastiness[itemIndex]);
33 }
34
35 // Option 3: Buy item with coupon (half price) if we have enough money and coupons
36 int halfPrice = price[itemIndex] / 2;
37 if (remainingAmount >= halfPrice && remainingCoupons > 0) {
38 maxValue = max(maxValue,
39 calculateMaxTastiness(itemIndex + 1,
40 remainingAmount - halfPrice,
41 remainingCoupons - 1) + tastiness[itemIndex]);
42 }
43
44 // Memoize and return result
45 dp[itemIndex][remainingAmount][remainingCoupons] = maxValue;
46 return maxValue;
47 };
48
49 // Start recursion from first item with full budget and all coupons
50 return calculateMaxTastiness(0, maxAmount, maxCoupons);
51 }
52
53private:
54 // Memoization table: [item index][remaining amount][remaining coupons]
55 // Constraints: max 100 items, max 1000 amount, max 5 coupons
56 int dp[101][1001][6];
57};
58
1// Memoization table: [item index][remaining amount][remaining coupons]
2// Constraints: max 100 items, max 1000 amount, max 5 coupons
3let dp: number[][][] = [];
4
5function maxTastiness(price: number[], tastiness: number[], maxAmount: number, maxCoupons: number): number {
6 const itemCount = price.length;
7
8 // Initialize memoization table with -1 to indicate uncomputed states
9 // Using -1 instead of 0 since 0 could be a valid result
10 dp = Array(101).fill(null).map(() =>
11 Array(1001).fill(null).map(() =>
12 Array(6).fill(-1)
13 )
14 );
15
16 // Recursive function to calculate maximum tastiness with memoization
17 const calculateMaxTastiness = (itemIndex: number, remainingAmount: number, remainingCoupons: number): number => {
18 // Base case: no more items to consider
19 if (itemIndex === itemCount) {
20 return 0;
21 }
22
23 // Return memoized result if already computed
24 if (dp[itemIndex][remainingAmount][remainingCoupons] !== -1) {
25 return dp[itemIndex][remainingAmount][remainingCoupons];
26 }
27
28 // Option 1: Skip the current item
29 let maxValue = calculateMaxTastiness(itemIndex + 1, remainingAmount, remainingCoupons);
30
31 // Option 2: Buy the item at full price if we have enough money
32 if (remainingAmount >= price[itemIndex]) {
33 maxValue = Math.max(
34 maxValue,
35 calculateMaxTastiness(
36 itemIndex + 1,
37 remainingAmount - price[itemIndex],
38 remainingCoupons
39 ) + tastiness[itemIndex]
40 );
41 }
42
43 // Option 3: Buy the item with a coupon (half price) if we have enough money and coupons
44 const halfPrice = Math.floor(price[itemIndex] / 2);
45 if (remainingAmount >= halfPrice && remainingCoupons > 0) {
46 maxValue = Math.max(
47 maxValue,
48 calculateMaxTastiness(
49 itemIndex + 1,
50 remainingAmount - halfPrice,
51 remainingCoupons - 1
52 ) + tastiness[itemIndex]
53 );
54 }
55
56 // Memoize and return the result
57 dp[itemIndex][remainingAmount][remainingCoupons] = maxValue;
58 return maxValue;
59 };
60
61 // Start recursion from the first item with full budget and all coupons
62 return calculateMaxTastiness(0, maxAmount, maxCoupons);
63}
64
Time and Space Complexity
Time Complexity: O(n × maxAmount × maxCoupons)
The time complexity is determined by the memoized recursive function dfs(i, j, k)
:
- Parameter
i
ranges from0
ton-1
(wheren = len(price)
) - Parameter
j
ranges from0
tomaxAmount
- Parameter
k
ranges from0
tomaxCoupons
Due to the @cache
decorator, each unique combination of (i, j, k)
is computed only once. The total number of unique states is n × (maxAmount + 1) × (maxCoupons + 1)
. For each state, the function performs constant time operations (comparisons and arithmetic), resulting in O(n × maxAmount × maxCoupons)
time complexity.
Space Complexity: O(n × maxAmount × maxCoupons)
The space complexity consists of:
- Memoization cache: The
@cache
decorator stores all computed states, which requiresO(n × maxAmount × maxCoupons)
space - Recursion call stack: In the worst case, the recursion depth is
O(n)
(when processing all items sequentially)
Since the memoization cache dominates the space usage, the overall space complexity is O(n × maxAmount × maxCoupons)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Coupon Usage Strategy
A common mistake is assuming that coupons should always be used on the most expensive items to maximize savings. This greedy approach can lead to suboptimal solutions.
Why it's wrong: The optimal strategy isn't just about saving the most money—it's about maximizing tastiness within budget constraints. Sometimes using a coupon on a cheaper item allows you to buy more total items, resulting in higher overall tastiness.
Example scenario:
- Budget: 10, Coupons: 1
- Fruit A: price=8, tastiness=5
- Fruit B: price=6, tastiness=4
- Fruit C: price=4, tastiness=3
If you use the coupon on the most expensive (Fruit A), you pay 4, leaving 6 for one more fruit (B), total tastiness = 9. If you use the coupon on Fruit B, you pay 3, leaving 7 for Fruit C (4), total tastiness = 7. But if you buy B at full price (6) and use coupon on C (2), you spend 8 total and get tastiness = 7. The first approach (coupon on most expensive) happens to be optimal here, but this isn't always the case.
Solution: The dynamic programming approach correctly explores all possibilities rather than making greedy assumptions about coupon usage.
2. Integer Division Confusion
When calculating half price with a coupon, using floating-point division instead of integer division can cause errors.
Incorrect:
half_price = price[fruit_index] / 2 # Results in float if remaining_amount >= half_price: # Float comparison
Correct:
half_price = price[fruit_index] // 2 # Integer division if remaining_amount >= half_price: # Integer comparison
Why it matters: If a fruit costs 11, the half price should be 5 (not 5.5). Using float division could lead to budget calculation errors and incorrect feasibility checks.
3. State Space Explosion Without Memoization
Without the @cache
decorator, the recursive solution would have exponential time complexity, making it impractical for larger inputs.
Problem without memoization:
def dfs(fruit_index, remaining_amount, remaining_coupons):
# Same logic but no caching
...
This would recompute the same subproblems multiple times. For instance, you might reach state (i=3, money=5, coupons=2) through many different purchasing paths.
Solution: Always use memoization (@cache
or manual dictionary) to ensure each unique state is computed only once.
4. Forgetting to Clear Cache Between Test Cases
When running multiple test cases in the same execution context, the @cache
decorator persists across calls, potentially returning incorrect cached results from previous test cases.
Fix:
class Solution:
def maxTastiness(self, price, tastiness, maxAmount, maxCoupons):
@cache
def dfs(fruit_index, remaining_amount, remaining_coupons):
# ... implementation ...
result = dfs(0, maxAmount, maxCoupons)
dfs.cache_clear() # Clear cache after solving
return result
5. Off-by-One Errors in Coupon Check
A subtle bug can occur when checking coupon availability:
Incorrect:
if remaining_amount >= half_price and remaining_coupons >= 0: # Wrong!
Correct:
if remaining_amount >= half_price and remaining_coupons > 0: # Must have at least 1
The condition should be remaining_coupons > 0
(or just remaining_coupons
in Python), not >= 0
, because you need at least one coupon to use it.
Which algorithm should you use to find a node that is close to the root of the tree?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!