2969. Minimum Number of Coins for Fruits II đź”’
Problem Description
You are at a fruit market with different types of exotic fruits on display. The fruits are arranged in a line and numbered starting from 1 (1-indexed).
Each fruit has a price, given in an array prices
, where prices[i]
represents the number of coins needed to purchase the i-th
fruit.
The market has a special offer:
- When you purchase the
i-th
fruit forprices[i]
coins, you get the nexti
fruits for free
For example:
- If you buy fruit 1, you get fruit 2 for free (1 fruit free)
- If you buy fruit 2, you get fruits 3 and 4 for free (2 fruits free)
- If you buy fruit 3, you get fruits 4, 5, and 6 for free (3 fruits free)
Important note: Even if you can take a fruit for free due to a previous purchase, you can still choose to buy it with coins to activate its own offer.
Your goal is to find the minimum number of coins needed to acquire all the fruits in the market.
The problem essentially asks you to strategically decide which fruits to buy with coins (to activate their offers) and which fruits to take for free (from previous offers), such that you end up with all fruits while spending the minimum amount of coins.
Intuition
Let's think about this problem backwards. If we're standing at fruit i
, what's the minimum cost to get all fruits from position i
to the end?
When we buy fruit i
, we get the next i
fruits for free. This means if we buy fruit i
, we can skip paying for fruits i+1, i+2, ..., i+i
. But we still need to ensure all fruits after position i+i
are obtained somehow.
This leads us to a key insight: if we buy fruit i
, we need to make sure all fruits from position i+i+1
onwards are covered. The way to do this is to buy some fruit j
where i < j ≤ 2i+1
(within our free range or just after it) that will help cover the remaining fruits.
So for each fruit i
, the minimum cost is:
f[i] = prices[i-1] + min(f[j])
where j
ranges from i+1
to 2i+1
Working backwards from the last fruit makes sense because:
- The last fruit only costs its price (no fruits after it to worry about)
- Each previous fruit needs to consider the optimal ways to cover all fruits after it
The challenge is that for each position i
, we need to find the minimum value among f[i+1], f[i+2], ..., f[2i+1]
. As we move from right to left (decreasing i
), this range keeps changing - the left boundary increases while the right boundary decreases.
This is essentially a sliding window problem where we need to track the minimum value in a window that's constantly shifting and shrinking. A monotonic deque is perfect for this - it maintains elements in increasing order, allowing us to efficiently:
- Remove elements that fall outside our current window
- Find the minimum value (always at the front)
- Add new elements while maintaining the monotonic property
Learn more about Queue, Dynamic Programming, Monotonic Queue and Heap (Priority Queue) patterns.
Solution Approach
We implement a dynamic programming solution optimized with a monotonic deque. The solution works backwards from the last fruit to the first.
Core Algorithm:
-
Initialize a deque
q
to maintain indices of fruits in a way that helps us find the minimum cost efficiently. -
Iterate backwards from fruit
n
to fruit1
:- For each fruit
i
, we need to find the minimum cost among fruits in range[i+1, 2i+1]
- For each fruit
-
Maintain the sliding window using the deque:
- Remove outdated elements: While the front of the deque contains indices greater than
2i+1
, remove them (they're outside our valid range) - Update the current fruit's cost: If
i ≤ (n-1)/2
, add the minimum cost from the valid range toprices[i-1]
. The minimum is always atprices[q[0]-1]
due to our monotonic property - Maintain monotonic property: Before adding index
i
to the deque, remove all indices from the back whose corresponding prices are greater than or equal toprices[i-1]
. This ensures the deque remains monotonically increasing by price values - Add current index: Append
i
to the deque
- Remove outdated elements: While the front of the deque contains indices greater than
Key Implementation Details:
-
We reuse the
prices
array for dynamic programming values, whereprices[i-1]
ultimately stores the minimum cost to get all fruits starting from fruiti
-
The condition
i ≤ (n-1)/2
is crucial:- When
i > (n-1)/2
, buying fruiti
gives us enough free fruits to cover all remaining fruits (since we geti
free fruits and there are at mostn-i
fruits after positioni
) - When
i ≤ (n-1)/2
, we need to buy additional fruits later, so we add the minimum cost from our valid range
- When
-
The monotonic deque optimization reduces time complexity from
O(n²)
toO(n)
because:- Each element is added to the deque once and removed at most once
- Finding the minimum is
O(1)
(always at the front of the deque)
The final answer is prices[0]
, which represents the minimum cost to acquire all fruits starting from fruit 1.
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 concrete example with prices = [3, 1, 2]
(3 fruits total).
Initial Setup:
- We have 3 fruits with prices: fruit 1 costs 3, fruit 2 costs 1, fruit 3 costs 2
- We'll work backwards from fruit 3 to fruit 1
- Initialize an empty deque
q = []
Step 1: Process fruit 3 (i = 3)
- Current window range: [4, 7] (i+1 to 2i+1)
- Since there are no fruits beyond position 3, no additional cost needed
- Check condition: i = 3 > (n-1)/2 = 1, so
prices[2]
remains 2 - Update deque: Add index 3 to deque →
q = [3]
- Current DP state:
prices = [3, 1, 2]
Step 2: Process fruit 2 (i = 2)
- Current window range: [3, 5] (i+1 to 2i+1)
- Remove outdated: Front element 3 ≤ 5, so keep it
- Check condition: i = 2 > (n-1)/2 = 1, so
prices[1]
remains 1- This means buying fruit 2 gives us 2 free fruits, covering fruits 3 and 4 (enough to get all remaining fruits)
- Maintain monotonic property:
prices[2]
= 2 >prices[1]
= 1, so remove 3 from back - Update deque: Add index 2 →
q = [2]
- Current DP state:
prices = [3, 1, 2]
Step 3: Process fruit 1 (i = 1)
- Current window range: [2, 3] (i+1 to 2i+1)
- Remove outdated: Front element 2 ≤ 3, so keep it
- Check condition: i = 1 ≤ (n-1)/2 = 1, so we need to add minimum cost
- Minimum in range is at
q[0] = 2
, soprices[1-1]
= 1 - Update:
prices[0] = 3 + 1 = 4
- Minimum in range is at
- Maintain monotonic property:
prices[1]
= 1 <prices[0]
= 4, so nothing to remove - Update deque: Add index 1 →
q = [2, 1]
- Final DP state:
prices = [4, 1, 2]
Result: The minimum cost is prices[0] = 4
Verification: Let's verify this is correct by checking different strategies:
- Buy fruit 1 only (cost 3): Get fruit 2 free, but need to buy fruit 3 → Total: 3 + 2 = 5
- Buy fruits 1 and 2 (cost 3 + 1): Fruit 1 gives fruit 2 free (redundant), fruit 2 gives fruits 3 and 4 free → Total: 4 ✓
- Buy fruit 2 only (cost 1): Get fruits 3 and 4 free, but fruit 1 isn't covered → Invalid
- Buy all fruits: 3 + 1 + 2 = 6
The optimal strategy is indeed to buy fruits 1 and 2 for a total cost of 4 coins.
Solution Implementation
1class Solution:
2 def minimumCoins(self, prices: List[int]) -> int:
3 # Get the number of items
4 n = len(prices)
5
6 # Deque to maintain indices of potential minimum values
7 # Acts as a monotonic deque for sliding window optimization
8 dq = deque()
9
10 # Process items from right to left (backwards)
11 for current_index in range(n, 0, -1):
12 # Remove indices that are outside the valid range
13 # Valid range: items that can be obtained for free when buying at current_index
14 # When buying item at index i (1-indexed), you get items i+1 to 2*i for free
15 while dq and dq[0] > current_index * 2 + 1:
16 dq.popleft()
17
18 # If current item is in the first half of the array
19 # Add the minimum cost from the valid range to current price
20 if current_index <= (n - 1) // 2:
21 # Add the minimum cost from the deque to current price
22 # This represents the minimum cost to buy all items from current_index onwards
23 prices[current_index - 1] += prices[dq[0] - 1]
24
25 # Maintain monotonic property of the deque
26 # Remove elements from the back that have greater or equal cost
27 while dq and prices[dq[-1] - 1] >= prices[current_index - 1]:
28 dq.pop()
29
30 # Add current index to the deque
31 dq.append(current_index)
32
33 # Return the minimum cost to buy all items starting from the first item
34 return prices[0]
35
1class Solution {
2 public int minimumCoins(int[] prices) {
3 int n = prices.length;
4
5 // Monotonic deque to maintain indices of potential optimal choices
6 // Stores 1-indexed positions for easier calculation
7 Deque<Integer> deque = new ArrayDeque<>();
8
9 // Process items from right to left (1-indexed)
10 for (int currentIndex = n; currentIndex > 0; currentIndex--) {
11
12 // Remove indices that are out of range for current position
13 // An item at position i can get free items up to position 2*i + 1
14 while (!deque.isEmpty() && deque.peek() > currentIndex * 2 + 1) {
15 deque.poll();
16 }
17
18 // For items in the first half, add the minimum cost from valid future positions
19 // Only items up to position (n-1)/2 need to consider future costs
20 if (currentIndex <= (n - 1) / 2) {
21 int optimalFutureIndex = deque.peek();
22 prices[currentIndex - 1] += prices[optimalFutureIndex - 1];
23 }
24
25 // Maintain monotonic property: remove elements with higher or equal cost
26 // This ensures the deque front always has the minimum cost option
27 while (!deque.isEmpty() && prices[deque.peekLast() - 1] >= prices[currentIndex - 1]) {
28 deque.pollLast();
29 }
30
31 // Add current index to the deque for future consideration
32 deque.offer(currentIndex);
33 }
34
35 // Return the minimum cost to acquire all items starting from index 0
36 return prices[0];
37 }
38}
39
1class Solution {
2public:
3 int minimumCoins(vector<int>& prices) {
4 int n = prices.size();
5
6 // Monotonic deque to maintain indices of potential minimum costs
7 // Stores 1-indexed positions for consistency with the problem
8 deque<int> monotonicQueue;
9
10 // Process items from right to left (backward DP)
11 for (int currentPos = n; currentPos >= 1; --currentPos) {
12 // Remove indices that are out of range for current position
13 // If we buy at currentPos, we can get items up to currentPos + currentPos for free
14 // So we need costs from positions > currentPos * 2 + 1
15 while (!monotonicQueue.empty() && monotonicQueue.front() > currentPos * 2 + 1) {
16 monotonicQueue.pop_front();
17 }
18
19 // For positions in the first half, we need to consider buying subsequent items
20 // Add the minimum cost from reachable positions to current price
21 if (currentPos <= (n - 1) / 2) {
22 int minCostIndex = monotonicQueue.front();
23 prices[currentPos - 1] += prices[minCostIndex - 1];
24 }
25
26 // Maintain monotonic property: remove elements with cost >= current cost
27 // This ensures the deque front always has the minimum cost
28 while (!monotonicQueue.empty() &&
29 prices[monotonicQueue.back() - 1] >= prices[currentPos - 1]) {
30 monotonicQueue.pop_back();
31 }
32
33 // Add current position to the deque
34 monotonicQueue.push_back(currentPos);
35 }
36
37 // Return the minimum cost to get all items starting from position 1
38 return prices[0];
39 }
40};
41
1// Global variables for deque implementation
2let frontNode: Node<number> | null = null;
3let backNode: Node<number> | null = null;
4let dequeSize: number = 0;
5
6// Node structure for doubly linked list
7interface Node<T> {
8 value: T;
9 next: Node<T> | null;
10 prev: Node<T> | null;
11}
12
13// Create a new node
14function createNode<T>(value: T): Node<T> {
15 return {
16 value: value,
17 next: null,
18 prev: null
19 };
20}
21
22// Add element to the front of deque
23function pushFront(val: number): void {
24 const newNode = createNode(val);
25 if (isEmpty()) {
26 frontNode = newNode;
27 backNode = newNode;
28 } else {
29 newNode.next = frontNode;
30 frontNode!.prev = newNode;
31 frontNode = newNode;
32 }
33 dequeSize++;
34}
35
36// Add element to the back of deque
37function pushBack(val: number): void {
38 const newNode = createNode(val);
39 if (isEmpty()) {
40 frontNode = newNode;
41 backNode = newNode;
42 } else {
43 newNode.prev = backNode;
44 backNode!.next = newNode;
45 backNode = newNode;
46 }
47 dequeSize++;
48}
49
50// Remove and return element from the front of deque
51function popFront(): number | undefined {
52 if (isEmpty()) {
53 return undefined;
54 }
55 const value = frontNode!.value;
56 frontNode = frontNode!.next;
57 if (frontNode !== null) {
58 frontNode.prev = null;
59 } else {
60 backNode = null;
61 }
62 dequeSize--;
63 return value;
64}
65
66// Remove and return element from the back of deque
67function popBack(): number | undefined {
68 if (isEmpty()) {
69 return undefined;
70 }
71 const value = backNode!.value;
72 backNode = backNode!.prev;
73 if (backNode !== null) {
74 backNode.next = null;
75 } else {
76 frontNode = null;
77 }
78 dequeSize--;
79 return value;
80}
81
82// Get the value at the front without removing it
83function frontValue(): number | undefined {
84 return frontNode?.value;
85}
86
87// Get the value at the back without removing it
88function backValue(): number | undefined {
89 return backNode?.value;
90}
91
92// Get the current size of deque
93function getSize(): number {
94 return dequeSize;
95}
96
97// Check if deque is empty
98function isEmpty(): boolean {
99 return dequeSize === 0;
100}
101
102// Reset deque to initial state
103function resetDeque(): void {
104 frontNode = null;
105 backNode = null;
106 dequeSize = 0;
107}
108
109/**
110 * Find minimum coins needed to buy all fruits
111 * Uses dynamic programming with monotonic deque optimization
112 * @param prices - Array of fruit prices
113 * @returns Minimum coins needed
114 */
115function minimumCoins(prices: number[]): number {
116 // Reset deque for new calculation
117 resetDeque();
118
119 const n = prices.length;
120
121 // Process fruits from right to left (backwards)
122 for (let i = n; i >= 1; i--) {
123 // Remove indices that are out of range for current fruit
124 // A fruit at position i can get free fruits up to position 2*i + 1
125 while (getSize() > 0 && frontValue()! > i * 2 + 1) {
126 popFront();
127 }
128
129 // If current fruit is in the first half, add the minimum cost from deque
130 // This represents the minimum cost to buy remaining fruits
131 if (i <= Math.floor((n - 1) / 2)) {
132 prices[i - 1] += prices[frontValue()! - 1];
133 }
134
135 // Maintain monotonic property: remove elements with higher or equal cost
136 // Keep deque in increasing order of cost
137 while (getSize() > 0 && prices[backValue()! - 1] >= prices[i - 1]) {
138 popBack();
139 }
140
141 // Add current index to deque
142 pushBack(i);
143 }
144
145 // Return the minimum cost starting from the first fruit
146 return prices[0];
147}
148
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array prices
.
The algorithm iterates through the array once in reverse order (from n
to 1
), performing O(1)
amortized operations with the deque in each iteration. While there are nested while loops inside the main loop, each element can be added to and removed from the deque at most once throughout the entire execution. The first while loop removes elements that are out of range, and the second while loop maintains a monotonic property by removing elements. Since each of the n
elements is pushed exactly once and popped at most once, the total number of deque operations across all iterations is O(n)
.
The space complexity is O(n)
, where n
is the length of the array prices
.
The primary space usage comes from the deque q
, which in the worst case can store up to n
elements when all elements satisfy the monotonic property maintained by the algorithm. The algorithm modifies the input array prices
in-place, but since this is already provided as input, it doesn't count as additional space. No other data structures scale with the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Index Confusion Between 0-indexed Array and 1-indexed Problem
The problem describes fruits numbered from 1 (1-indexed), but the prices
array is 0-indexed. This creates multiple opportunities for off-by-one errors.
Pitfall Example:
# Incorrect: Mixing up indices
for current_index in range(n, 0, -1):
while dq and dq[0] > current_index * 2 + 1: # Using 1-indexed logic
dq.popleft()
prices[current_index] += prices[dq[0]] # Wrong! Array access should be index-1
Solution:
- Consistently track whether you're working with 1-indexed positions (fruit numbers) or 0-indexed positions (array indices)
- When storing indices in the deque, decide on one convention and stick to it
- Add clear comments indicating which indexing system each variable uses
2. Incorrect Sliding Window Range Calculation
When buying fruit i
(1-indexed), you get the next i
fruits free, meaning fruits [i+1, i+i]
are free. The range for finding minimum cost should be [i+1, min(2*i+1, n)]
.
Pitfall Example:
# Incorrect: Wrong upper bound while dq and dq[0] > current_index * 2: # Should be 2*i + 1 dq.popleft() # Or forgetting to cap at n while dq and dq[0] > current_index * 2 + 1: # Doesn't handle when 2*i+1 > n dq.popleft()
Solution:
- The upper bound should be
2*i + 1
(for 1-indexed) ormin(2*i + 1, n)
if being explicit - Since we process backwards and maintain the deque properly, indices beyond
n
naturally won't exist in the deque
3. Forgetting the Special Case for Large Indices
When i > (n-1)/2
, buying fruit i
gives enough free fruits to cover all remaining fruits, so we don't need to add any additional cost.
Pitfall Example:
# Incorrect: Always adding minimum cost
for current_index in range(n, 0, -1):
if dq: # Wrong condition!
prices[current_index - 1] += prices[dq[0] - 1]
Solution:
- Only add the minimum cost when
current_index <= (n-1)//2
- This ensures we don't unnecessarily add costs when the current purchase already covers all remaining fruits
4. Improper Deque Maintenance for Monotonic Property
The deque should maintain indices in increasing order of their corresponding prices to ensure O(1)
minimum finding.
Pitfall Example:
# Incorrect: Using wrong comparison while dq and prices[dq[-1] - 1] > prices[current_index - 1]: # Should be >= dq.pop() # Or comparing indices instead of values while dq and dq[-1] >= current_index: # Wrong! Should compare prices dq.pop()
Solution:
- Use
>=
instead of>
to handle equal values properly and maintain strict monotonicity - Always compare the actual price values, not the indices themselves
- This ensures the deque front always contains the index with minimum price in the valid range
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
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!