LeetCode 3479 - Fruits Into Baskets III
This problem gives us two arrays of equal length: - fruits[i] represents the quantity of the i-th fruit type. - baskets[j] represents the capacity of the j-th basket. We process the fruit types from left to right.
Difficulty: 🟡 Medium
Topics: Array, Binary Search, Segment Tree, Ordered Set
Solution
LeetCode 3479: Fruits Into Baskets III
Problem Understanding
This problem gives us two arrays of equal length:
fruits[i]represents the quantity of thei-thfruit type.baskets[j]represents the capacity of thej-thbasket.
We process the fruit types from left to right. For each fruit quantity, we must place it into the leftmost available basket whose capacity is at least as large as the fruit quantity.
A basket can only be used once. After a basket is assigned to a fruit type, it becomes unavailable for all future fruit types.
If no available basket has sufficient capacity, that fruit type remains unplaced.
The goal is to return the total number of fruit types that remain unplaced after processing all fruits.
The key detail is the phrase "leftmost available basket". We are not simply looking for any basket with enough capacity. Among all currently unused baskets that can hold the fruit, we must choose the one with the smallest index.
What the Constraints Tell Us
The array length can be as large as 100,000.
A straightforward simulation would, for every fruit, scan all baskets from left to right until finding a valid unused basket. In the worst case, this requires:
nfruitsnbasket checks per fruit
This leads to O(n²) time complexity, which would be far too slow for n = 100,000.
Therefore, we need a data structure that can efficiently:
- Determine whether any available basket can hold a given fruit.
- Find the leftmost available basket satisfying the capacity requirement.
- Mark a basket as used.
These requirements naturally suggest a segment tree.
Important Edge Cases
One important edge case occurs when every fruit is larger than every basket. In that situation, none of the fruits can be placed and the answer equals n.
Another important case occurs when multiple baskets can hold a fruit. The problem requires selecting the leftmost available one, not the smallest capacity one. Any solution that greedily chooses baskets based on capacity alone would be incorrect.
A third edge case occurs when a basket that could hold a future fruit gets consumed by an earlier fruit because it is the leftmost valid basket. The placement order is fixed by the problem statement, so we cannot reorder fruits or baskets.
The problem guarantees:
- Both arrays have the same length.
- All values are positive.
- There are no empty arrays.
These guarantees simplify implementation because we never need to handle invalid input.
Approaches
Brute Force
The most direct solution is to simulate the process exactly as described.
For each fruit:
- Scan baskets from left to right.
- Skip already-used baskets.
- Find the first basket whose capacity is at least the fruit quantity.
- Mark it as used.
- If no such basket exists, increment the answer.
This approach is obviously correct because it follows the problem statement literally.
However, in the worst case every fruit scans almost every basket. With n = 100,000, this results in approximately 10^10 operations, which is far beyond acceptable limits.
Key Insight
The challenge is not checking basket capacities. The challenge is efficiently finding the leftmost unused basket whose capacity is at least some threshold.
Suppose we build a segment tree where each node stores the maximum basket capacity in its range among baskets that are still available.
Then:
- If the maximum value in a range is smaller than the fruit quantity, we know no basket in that range can hold the fruit.
- If the maximum value is large enough, then some basket in that range can hold the fruit.
Using this information, we can descend the segment tree:
- Always explore the left child first.
- If the left child contains a valid basket, continue there.
- Otherwise move to the right child.
This guarantees that we find the leftmost available basket whose capacity is large enough.
Once a basket is used, we update its value in the segment tree to 0, effectively removing it from future consideration.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Scan baskets for every fruit |
| Optimal | O(n log n) | O(n) | Segment tree finds leftmost valid basket efficiently |
Algorithm Walkthrough
- Build a segment tree over the
basketsarray.
Each node stores the maximum basket capacity within its range. This allows us to quickly determine whether a range contains any basket capable of holding a given fruit.
2. Initialize a counter unplaced = 0.
This will track fruit types that cannot be assigned to any basket. 3. Process each fruit quantity from left to right.
The order is fixed by the problem statement, so we must handle fruits exactly in array order. 4. Check the segment tree root.
If the root maximum is smaller than the fruit quantity, then no available basket can hold this fruit. Increment unplaced and continue.
5. Otherwise search the segment tree.
Starting from the root:
- Examine the left child first.
- If the left child's maximum capacity is large enough, move into the left child.
- Otherwise move into the right child.
This search eventually reaches a leaf corresponding to the leftmost available basket whose capacity is at least the fruit quantity. 6. Mark that basket as used.
Update the leaf value to 0.
7. Recompute segment tree values while returning upward.
Each internal node stores the maximum of its two children.
8. Continue until all fruits have been processed.
9. Return unplaced.
Why it Works
The segment tree maintains the invariant that every node stores the maximum available basket capacity in its range.
If a node's maximum value is smaller than the current fruit quantity, no basket within that range can hold the fruit. Therefore that entire range can safely be ignored.
When searching, we always check the left child before the right child. Whenever the left child contains a valid basket, we continue searching there. Consequently, the first leaf reached is exactly the leftmost available basket capable of holding the fruit.
Because every placement follows the problem's required rule and every used basket is removed from future consideration, the algorithm always produces the correct result.
Python Solution
from typing import List
class Solution:
def numOfUnplacedFruits(self, fruits: List[int], baskets: List[int]) -> int:
n = len(baskets)
tree = [0] * (4 * n)
def build(node: int, left: int, right: int) -> None:
if left == right:
tree[node] = baskets[left]
return
mid = (left + right) // 2
build(node * 2, left, mid)
build(node * 2 + 1, mid + 1, right)
tree[node] = max(tree[node * 2], tree[node * 2 + 1])
def update(node: int, left: int, right: int, idx: int) -> None:
if left == right:
tree[node] = 0
return
mid = (left + right) // 2
if idx <= mid:
update(node * 2, left, mid, idx)
else:
update(node * 2 + 1, mid + 1, right, idx)
tree[node] = max(tree[node * 2], tree[node * 2 + 1])
def find_leftmost(node: int, left: int, right: int, target: int) -> int:
if left == right:
return left
mid = (left + right) // 2
if tree[node * 2] >= target:
return find_leftmost(node * 2, left, mid, target)
return find_leftmost(node * 2 + 1, mid + 1, right, target)
build(1, 0, n - 1)
unplaced = 0
for fruit in fruits:
if tree[1] < fruit:
unplaced += 1
continue
basket_index = find_leftmost(1, 0, n - 1, fruit)
update(1, 0, n - 1, basket_index)
return unplaced
The implementation begins by constructing a segment tree where each node stores the maximum available basket capacity in its interval.
The build function initializes the tree from the basket capacities.
The find_leftmost function performs the crucial search. Because each node stores a maximum value, it can determine whether the left subtree contains a valid basket. If it does, the search continues left. Otherwise, it moves right. This guarantees the leftmost valid basket is found.
After locating a basket, the update function sets that basket's value to zero, indicating that it is no longer available. The maximum values of all affected ancestors are then recomputed.
For every fruit, the root node immediately tells us whether any valid basket still exists. If not, the fruit remains unplaced. Otherwise, we locate and consume the required basket.
Go Solution
func numOfUnplacedFruits(fruits []int, baskets []int) int {
n := len(baskets)
tree := make([]int, 4*n)
var build func(int, int, int)
build = func(node, left, right int) {
if left == right {
tree[node] = baskets[left]
return
}
mid := (left + right) / 2
build(node*2, left, mid)
build(node*2+1, mid+1, right)
if tree[node*2] > tree[node*2+1] {
tree[node] = tree[node*2]
} else {
tree[node] = tree[node*2+1]
}
}
var update func(int, int, int, int)
update = func(node, left, right, idx int) {
if left == right {
tree[node] = 0
return
}
mid := (left + right) / 2
if idx <= mid {
update(node*2, left, mid, idx)
} else {
update(node*2+1, mid+1, right, idx)
}
if tree[node*2] > tree[node*2+1] {
tree[node] = tree[node*2]
} else {
tree[node] = tree[node*2+1]
}
}
var findLeftmost func(int, int, int, int) int
findLeftmost = func(node, left, right, target int) int {
if left == right {
return left
}
mid := (left + right) / 2
if tree[node*2] >= target {
return findLeftmost(node*2, left, mid, target)
}
return findLeftmost(node*2+1, mid+1, right, target)
}
build(1, 0, n-1)
unplaced := 0
for _, fruit := range fruits {
if tree[1] < fruit {
unplaced++
continue
}
idx := findLeftmost(1, 0, n-1, fruit)
update(1, 0, n-1, idx)
}
return unplaced
}
The Go implementation follows the same segment tree design as the Python version. Go slices are used to store the tree, and recursive closures implement build, search, and update operations.
Integer overflow is not a concern because basket capacities are at most 10^9, which comfortably fits within Go's int on LeetCode platforms. The algorithm does not perform arithmetic that could exceed these bounds.
Worked Examples
Example 1
Input
fruits = [4,2,5]
baskets = [3,5,4]
Initial segment tree maximum:
max = 5
| Fruit | Available Baskets | Chosen Basket | Result |
|---|---|---|---|
| 4 | [3,5,4] | index 1, capacity 5 | placed |
| 2 | [3,used,4] | index 0, capacity 3 | placed |
| 5 | [used,used,4] | none | unplaced |
State evolution:
| Step | Basket State |
|---|---|
| Initial | [3,5,4] |
| After fruit 4 | [3,used,4] |
| After fruit 2 | [used,used,4] |
| After fruit 5 | [used,used,4] |
Answer:
1
Example 2
Input
fruits = [3,6,1]
baskets = [6,4,7]
| Fruit | Available Baskets | Chosen Basket | Result |
|---|---|---|---|
| 3 | [6,4,7] | index 0, capacity 6 | placed |
| 6 | [used,4,7] | index 2, capacity 7 | placed |
| 1 | [used,4,used] | index 1, capacity 4 | placed |
State evolution:
| Step | Basket State |
|---|---|
| Initial | [6,4,7] |
| After fruit 3 | [used,4,7] |
| After fruit 6 | [used,4,used] |
| After fruit 1 | [used,used,used] |
Answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Each fruit performs one search and one update |
| Space | O(n) | Segment tree storage |
The segment tree contains O(n) nodes. For each fruit, we perform a root check, a search down the tree, and an update back up the tree. Both search and update require O(log n) time. Since there are n fruits, the total running time is O(n log n).
Test Cases
sol = Solution()
assert sol.numOfUnplacedFruits([4, 2, 5], [3, 5, 4]) == 1 # example 1
assert sol.numOfUnplacedFruits([3, 6, 1], [6, 4, 7]) == 0 # example 2
assert sol.numOfUnplacedFruits([1], [1]) == 0 # single basket fits
assert sol.numOfUnplacedFruits([2], [1]) == 1 # single basket fails
assert sol.numOfUnplacedFruits([5, 5, 5], [5, 5, 5]) == 0 # exact matches
assert sol.numOfUnplacedFruits([10, 10, 10], [1, 2, 3]) == 3 # no basket fits
assert sol.numOfUnplacedFruits([1, 2, 3], [10, 10, 10]) == 0 # all fit
assert sol.numOfUnplacedFruits([4, 4, 4], [4, 3, 4]) == 1 # one fruit unplaced
assert sol.numOfUnplacedFruits([2, 5, 4], [5, 4, 6]) == 0 # leftmost rule matters
assert sol.numOfUnplacedFruits([8, 1, 8], [8, 8, 1]) == 1 # earlier usage affects later fruits
assert sol.numOfUnplacedFruits([1000000000], [1000000000]) == 0 # maximum value
assert sol.numOfUnplacedFruits([7, 7, 7, 7], [7, 7, 7, 7]) == 0 # repeated equal values
Test Summary
| Test | Why |
|---|---|
[4,2,5], [3,5,4] |
Official example 1 |
[3,6,1], [6,4,7] |
Official example 2 |
[1], [1] |
Smallest successful input |
[2], [1] |
Smallest failing input |
| Equal capacities and fruits | Exact matching behavior |
| All fruits too large | No placements possible |
| All baskets huge | Every fruit can be placed |
[4,4,4], [4,3,4] |
One placement failure |
[2,5,4], [5,4,6] |
Validates leftmost selection |
[8,1,8], [8,8,1] |
Earlier allocations affect later results |
| Maximum values | Boundary value testing |
| Repeated values | Duplicate capacities and quantities |
Edge Cases
All Fruits Are Too Large
Consider:
fruits = [10, 10, 10]
baskets = [1, 2, 3]
Every fruit exceeds every basket capacity. A buggy implementation might repeatedly search the tree even though placement is impossible. The segment tree root immediately reveals that no valid basket exists, so each fruit is counted as unplaced.
Multiple Valid Baskets Exist
Consider:
fruits = [2]
baskets = [5, 6, 7]
All baskets can hold the fruit. The problem requires choosing the leftmost available basket, which is index 0. The search procedure always prioritizes the left subtree whenever it contains a valid basket, guaranteeing compliance with this rule.
Earlier Placements Affect Later Choices
Consider:
fruits = [8, 1, 8]
baskets = [8, 8, 1]
The first fruit must occupy basket 0 because it is the leftmost valid basket. The second fruit then occupies basket 1, leaving only basket 2 with capacity 1. The final fruit cannot be placed. Any algorithm that attempts to globally optimize basket usage instead of following the required leftmost rule would produce an incorrect result. The segment tree solution exactly simulates the mandated allocation order and therefore handles this situation correctly.