2944. Minimum Number of Coins for Fruits
Problem Description
You are given a 0-indexed integer array prices
where prices[i]
represents the number of coins needed to purchase the (i + 1)
-th fruit.
The fruit market has a special reward system:
- When you purchase the
(i + 1)
-th fruit forprices[i]
coins, you can get any number of the nexti
fruits for free.
For example:
- If you buy the 1st fruit (index 0), you get 0 fruits for free
- If you buy the 2nd fruit (index 1), you can get the next 1 fruit for free
- If you buy the 3rd fruit (index 2), you can get the next 2 fruits for free
- And so on...
An important note: Even if you can take a fruit for free, you still have the option to purchase it with coins to activate its reward and get subsequent fruits for free.
Your task is to find the minimum number of coins needed to acquire all the fruits in the array.
The strategy involves deciding which fruits to buy with coins and which to take for free, optimizing the total cost while ensuring all fruits are obtained.
Intuition
Let's think about this problem step by step. When we buy a fruit at position i
, we get the next i
fruits for free. This creates overlapping choices - we need to decide which fruits to buy and which to take for free.
The key insight is that we must buy at least some fruits to eventually cover all fruits in the array. For any fruit we buy, we need to consider: what's the optimal way to cover the remaining fruits after taking the free ones?
Consider buying fruit at index i
(the (i+1)
-th fruit). After buying it:
- We pay
prices[i]
coins - We can take the next
i+1
fruits for free (since it's the(i+1)
-th fruit) - This covers fruits from index
i+1
to index2i+1
But here's the crucial decision: among the fruits we can take for free, we might want to buy some of them anyway to get their free fruit benefits for covering fruits further ahead.
This naturally leads to a recursive structure: for each fruit position i
, we need to find the minimum cost to cover all fruits starting from position i
. When we buy fruit i
, we then need to decide which fruit to buy next from the range we could take for free.
The base case becomes clear: if buying fruit i
gives us enough free fruits to reach the end of the array (when i * 2 >= n
), we only need to pay for that fruit and we're done.
Otherwise, we buy fruit i
and then recursively find the minimum cost by trying all possible next purchase positions within our free range [i+1, 2i+1]
. The formula becomes: cost(i) = prices[i-1] + min(cost(j))
for all valid j
in the next purchase range.
Since we'll encounter the same subproblems multiple times (different paths might lead to buying the same fruit position), memoization helps us avoid redundant calculations.
Learn more about Queue, Dynamic Programming, Monotonic Queue and Heap (Priority Queue) patterns.
Solution Approach
The solution uses memoization search (top-down dynamic programming) to find the minimum cost to acquire all fruits.
We implement a recursive function dfs(i)
that represents the minimum number of coins needed to buy all fruits starting from the i
-th fruit (1-indexed). The answer to our problem is dfs(1)
.
Implementation Details:
-
Function Definition:
dfs(i)
computes the minimum cost to cover all fruits from positioni
onwards. -
Base Case:
- When
i * 2 >= len(prices)
, buying thei
-th fruit gives us enough free fruits to reach the end - We return
prices[i - 1]
(converting to 0-indexed array access) - This happens because buying the
i
-th fruit gives usi
free fruits, so we can cover up to position2i
- When
-
Recursive Case:
- We must buy fruit
i
, which costsprices[i - 1]
- After buying it, we can take fruits from position
i + 1
to2i
for free - But we need to buy at least one more fruit to continue covering the rest
- We try all possible next purchase positions
j
in the range[i + 1, 2i + 1]
- The formula becomes:
dfs(i) = prices[i - 1] + min(dfs(j) for j in range(i + 1, i * 2 + 2))
- Note:
range(i + 1, i * 2 + 2)
generates values fromi + 1
to2i + 1
inclusive
- We must buy fruit
-
Memoization:
- The
@cache
decorator automatically memoizes the results - This prevents recalculating
dfs(i)
for the samei
value multiple times - Without memoization, the time complexity would be exponential
- The
-
Starting Point:
- We call
dfs(1)
to find the minimum cost starting from the first fruit - Using 1-based indexing in the recursion makes the logic cleaner since the
i
-th fruit givesi
free fruits
- We call
The algorithm explores all valid purchasing strategies while avoiding redundant calculations through memoization, ensuring we find the optimal solution efficiently.
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 with prices = [3, 1, 2]
to illustrate the solution approach.
We need to find the minimum cost to acquire all 3 fruits. Let's trace through the recursive calls:
Step 1: Start with dfs(1)
- We're buying the 1st fruit (index 0), which costs 3 coins
- Buying the 1st fruit gives us 1 free fruit (covers position 2)
- We still need to cover position 3
- Next purchase options: positions 2 or 3 (range [2, 3])
- Calculate:
dfs(1) = 3 + min(dfs(2), dfs(3))
Step 2: Evaluate dfs(2)
- Buying the 2nd fruit (index 1) costs 1 coin
- Buying the 2nd fruit gives us 2 free fruits (would cover positions 3-4)
- Since
2 * 2 = 4 >= 3
(array length), this reaches the end - Base case:
dfs(2) = 1
Step 3: Evaluate dfs(3)
- Buying the 3rd fruit (index 2) costs 2 coins
- Buying the 3rd fruit gives us 3 free fruits (would cover positions 4-6)
- Since
3 * 2 = 6 >= 3
(array length), this reaches the end - Base case:
dfs(3) = 2
Step 4: Calculate final result
dfs(1) = 3 + min(1, 2) = 3 + 1 = 4
Verification: The optimal strategy is:
- Buy fruit 1 for 3 coins (get fruit 2 for free)
- Buy fruit 2 for 1 coin (get fruits 3 for free)
- Total cost: 3 + 1 = 4 coins
Alternative strategy (suboptimal):
- Buy fruit 1 for 3 coins (get fruit 2 for free)
- Take fruit 2 for free
- Buy fruit 3 for 2 coins
- Total cost: 3 + 2 = 5 coins
The algorithm correctly identifies that buying fruit 2 (even though we could take it for free) leads to the minimum total cost of 4 coins.
Solution Implementation
1class Solution:
2 def minimumCoins(self, prices: List[int]) -> int:
3 """
4 Calculate minimum coins needed to buy all fruits.
5 When buying fruit at index i (1-indexed), you get the next i fruits for free.
6
7 Args:
8 prices: List of fruit prices (0-indexed)
9
10 Returns:
11 Minimum total cost to acquire all fruits
12 """
13 from functools import cache
14
15 @cache
16 def dfs(current_index: int) -> int:
17 """
18 Find minimum cost starting from current_index (1-indexed).
19
20 Args:
21 current_index: Current position in 1-indexed format
22
23 Returns:
24 Minimum cost to buy all remaining fruits
25 """
26 # Base case: if buying this fruit covers all remaining fruits
27 # (current_index * 2 means we can get up to index current_index * 2 for free)
28 if current_index * 2 >= len(prices):
29 return prices[current_index - 1] # Convert to 0-indexed for prices array
30
31 # Recursive case: buy current fruit and choose optimal next purchase
32 # After buying at current_index, we get fruits up to current_index * 2 for free
33 # Next purchase must be from (current_index + 1) to (current_index * 2 + 1)
34 min_cost = float('inf')
35 for next_purchase_index in range(current_index + 1, current_index * 2 + 2):
36 min_cost = min(min_cost, dfs(next_purchase_index))
37
38 # Return cost of current fruit plus minimum cost of optimal next purchase
39 return prices[current_index - 1] + min_cost
40
41 # Start from fruit at index 1 (1-indexed)
42 return dfs(1)
43
1class Solution {
2 private int[] prices;
3 private int[] memo; // Memoization array for dynamic programming
4 private int numItems;
5
6 /**
7 * Finds the minimum cost to acquire all items.
8 * When buying item i (1-indexed), you get the next i items for free.
9 *
10 * @param prices Array of prices for each item (0-indexed)
11 * @return Minimum total cost to get all items
12 */
13 public int minimumCoins(int[] prices) {
14 this.numItems = prices.length;
15 this.memo = new int[numItems + 1]; // 1-indexed memoization array
16 this.prices = prices;
17
18 // Start DFS from item 1 (1-indexed)
19 return dfs(1);
20 }
21
22 /**
23 * Recursive DFS with memoization to find minimum cost starting from item i.
24 *
25 * @param currentItem Current item index (1-indexed)
26 * @return Minimum cost to acquire all items from currentItem onwards
27 */
28 private int dfs(int currentItem) {
29 // Base case: If buying this item covers all remaining items
30 // (i.e., currentItem + free items >= total items)
31 if (currentItem * 2 >= numItems) {
32 return prices[currentItem - 1]; // Convert to 0-indexed for prices array
33 }
34
35 // Check if already computed
36 if (memo[currentItem] == 0) {
37 // Initialize with a large value
38 memo[currentItem] = 1 << 30; // 2^30, serves as infinity
39
40 // Try all possible next purchase positions
41 // After buying item i, we get items [i+1, 2*i] for free
42 // So next purchase must be from item (i+1) to item (2*i+1)
43 for (int nextItem = currentItem + 1; nextItem <= currentItem * 2 + 1; nextItem++) {
44 int currentCost = prices[currentItem - 1] + dfs(nextItem);
45 memo[currentItem] = Math.min(memo[currentItem], currentCost);
46 }
47 }
48
49 return memo[currentItem];
50 }
51}
52
1class Solution {
2public:
3 int minimumCoins(vector<int>& prices) {
4 int n = prices.size();
5
6 // dp[i] represents the minimum cost to buy fruits from position i to the end
7 // Initialize with a large value (0x3f3f3f3f is commonly used as infinity)
8 int dp[n + 1];
9 memset(dp, 0x3f, sizeof(dp));
10
11 // Recursive function with memoization
12 // i represents the current fruit position (1-indexed)
13 function<int(int)> dfs = [&](int i) -> int {
14 // Base case: if buying fruit at position i gives enough free fruits to reach the end
15 // When i * 2 >= n, we can get all remaining fruits for free
16 if (i * 2 >= n) {
17 return prices[i - 1];
18 }
19
20 // If not computed yet (still has the initial large value)
21 if (dp[i] == 0x3f3f3f3f) {
22 // Try all possible next positions to buy
23 // After buying fruit at position i, we get i free fruits
24 // So next purchase can be at positions [i+1, i*2+1]
25 for (int j = i + 1; j <= i * 2 + 1; ++j) {
26 dp[i] = min(dp[i], prices[i - 1] + dfs(j));
27 }
28 }
29
30 return dp[i];
31 };
32
33 // Start from position 1 (1-indexed)
34 return dfs(1);
35 }
36};
37
1/**
2 * Finds the minimum cost to buy all fruits using a special offer system
3 * where buying fruit at index i allows getting the next i fruits for free
4 * @param prices - Array of fruit prices
5 * @returns Minimum total cost to buy all fruits
6 */
7function minimumCoins(prices: number[]): number {
8 const totalFruits: number = prices.length;
9 // Memoization array to store computed results for each position
10 // Index represents the fruit position (1-indexed), value represents minimum cost
11 const memoizedCosts: number[] = Array(totalFruits + 1).fill(0);
12
13 /**
14 * Recursive function to calculate minimum cost starting from fruit at position currentIndex
15 * @param currentIndex - Current fruit position (1-indexed)
16 * @returns Minimum cost to buy all remaining fruits starting from currentIndex
17 */
18 const calculateMinCost = (currentIndex: number): number => {
19 // Base case: if buying current fruit covers all remaining fruits
20 // (when buying fruit i, we get next i fruits free, so 2*i covers all)
21 if (currentIndex * 2 >= totalFruits) {
22 return prices[currentIndex - 1];
23 }
24
25 // Check if result is already computed (memoization)
26 if (memoizedCosts[currentIndex] === 0) {
27 // Initialize with a large value to find minimum
28 memoizedCosts[currentIndex] = 1 << 30;
29
30 // Try all possible next purchase positions
31 // After buying fruit at currentIndex, we get currentIndex free fruits
32 // So next purchase can be at any position from currentIndex+1 to currentIndex*2+1
33 for (let nextIndex = currentIndex + 1; nextIndex <= currentIndex * 2 + 1; ++nextIndex) {
34 const totalCost: number = prices[currentIndex - 1] + calculateMinCost(nextIndex);
35 memoizedCosts[currentIndex] = Math.min(memoizedCosts[currentIndex], totalCost);
36 }
37 }
38
39 return memoizedCosts[currentIndex];
40 };
41
42 // Start from the first fruit (1-indexed)
43 return calculateMinCost(1);
44}
45
Time and Space Complexity
Time Complexity: O(n²)
The recursive function dfs(i)
explores different purchasing strategies. For each position i
, it considers all possible next positions in the range [i+1, min(2*i+1, n)]
. In the worst case, when i
is small, this range can contain up to i+1
elements.
- At position 1: we explore up to 2 options
- At position 2: we explore up to 3 options
- At position
i
: we explore up toi+1
options
Due to memoization with @cache
, each state i
is computed only once. There are n
possible states (positions 1 through n
), and for each state, we iterate through at most O(n)
next positions in the worst case. Therefore, the overall time complexity is O(n²)
.
Space Complexity: O(n)
The space complexity consists of:
- The recursion call stack depth:
O(n)
in the worst case when we traverse from position 1 to positionn
- The memoization cache:
O(n)
for storing results of up ton
different positions
Since both components are O(n)
, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Index Confusion Between 0-based and 1-based Systems
The Pitfall:
The most common mistake in this problem is mixing up 0-based array indexing with the 1-based fruit numbering system. The problem states that the (i + 1)
-th fruit gives i
free fruits, but the array prices
uses 0-based indexing. This creates confusion when:
- Accessing the prices array (0-based)
- Calculating how many free fruits you get (1-based logic)
- Determining the range of next possible purchases
Example of the mistake:
# Incorrect implementation mixing indices
def dfs(i): # If i is 0-based
if i * 2 >= len(prices): # Wrong! This uses 0-based logic
return prices[i]
min_cost = float('inf')
# Wrong range calculation
for j in range(i + 1, i * 2 + 1):
min_cost = min(min_cost, dfs(j))
return prices[i] + min_cost
The Solution: Stick to one indexing system within the recursive function. The correct approach uses 1-based indexing for the DFS logic:
dfs(1)
represents buying the 1st fruit (atprices[0]
)- Buying the
i
-th fruit (1-based) givesi
free fruits - Access prices using
prices[i - 1]
to convert to 0-based - The range of free fruits extends from position
i + 1
to2i
2. Incorrect Range for Next Purchase Options
The Pitfall:
After buying fruit at position i
, you get the next i
fruits for free (positions i+1
to 2i
). The mistake is incorrectly calculating which fruits you can choose to buy next.
Example of the mistake:
# Incorrect: Only considering fruits within the free range
for j in range(i + 1, 2 * i + 1): # Missing the (2i + 1)-th fruit
min_cost = min(min_cost, dfs(j))
The Solution:
You can buy any fruit from position i + 1
to 2i + 1
(inclusive). Even though fruits from i + 1
to 2i
can be taken for free, you might want to buy them to activate their rewards. The (2i + 1)
-th fruit is the first fruit that must be purchased if you want it (it's not covered by the free range).
3. Forgetting the Base Case Boundary
The Pitfall: Not correctly identifying when buying a single fruit can cover all remaining fruits.
Example of the mistake:
# Incorrect base case
if current_index >= len(prices): # Wrong condition
return 0
The Solution:
The base case occurs when current_index * 2 >= len(prices)
. This means buying the fruit at current_index
provides enough free fruits to reach the end of the array. At this point, we only need to pay for the current fruit: prices[current_index - 1]
.
In a binary min heap, the minimum element can be found in:
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!