DSA sheet · Arrays · Sliding Window pattern
Fruits Into Baskets
This problem hides a sliding window inside a story about a farm. Once you see through the story, it asks: "what is the longest subarray with at most 2 different values?" The teacher first writes a brute force that restarts from every tree with a fresh hashmap. She explains where to restart after getting stuck, and why a hashmap with counts is needed instead of a set. Then she turns it into a sliding window that shrinks from the left instead of restarting.
Why it matters: this is the first window in the sheet whose rule is about how many distinct values are inside, so the window's "summary" is a frequency map instead of a sum. The same shape solves Longest Substring with K Unique Characters and is the building block of Subarrays with K Different Integers.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting (sliding window, frequency maps)
- Part A · Brute force: start at every tree, fresh map each time
- Part B · Optimal: sliding window with a frequency map
- Part C · Revision page
Part 0 · Before starting
What is a subarray?
A subarray is a piece of the array whose elements are next to each other (contiguous): a start index, an end index, and everything in between, with nothing skipped. (In a string, the same thing is a substring.) Its length is end − start + 1. In this problem, "picking fruit from consecutive trees" is exactly choosing a subarray.
The brute force over all windows, and why it repeats work
Two loops can try every start and stretch to the right from it: O(n²). The waste: if we stretch from index 0 over [1, 1, 2] and get stuck, the next start (index 1) re-adds 1 and 2, which we had already put into our map. Sliding window keeps what's already in the window and only fixes the edges.
The window [left..right]: expand and shrink
A window is the current subarray between two pointers, left and right. We keep a summary of what's inside so we don't re-scan it.
- Expansion phase (done by
right): move right forward and add the new element to the summary. - Shrinking phase (done by
left): remove nums[left] from the summary and move left forward.
Both pointers start at index 0 and only ever move forward.
Fixed-size vs variable-size windows
| Fixed-size window | Variable-size window | |
|---|---|---|
| question looks like | "every subarray of size k…" | "longest / shortest / how many subarrays such that…" |
| how it moves | add the new element; once size passes k, remove the one k steps back | grow until the rule breaks, then shrink until it holds again |
| this problem? | no size is given | yes |
Longest vs shortest vs count
| kind | shrink while… | record the answer… |
|---|---|---|
| longest valid (this problem) | the window is invalid | after shrinking: best = max(best, right − left + 1) |
| shortest valid | the window is still valid | inside the while, before removing: best = min(…) |
| count of valid | the window is invalid | after shrinking: count += right − left + 1 |
For "exactly K different values" (problem 8 in this sheet) the trick is exactly(K) = atMost(K) − atMost(K − 1). This problem is already an "at most 2", so we don't need it.
Why the rule must be monotonic
Sliding window works only if adding an element can only make the window worse or equal, and removing one can only make it better or equal. Here the rule is "at most 2 distinct values": adding a tree can only keep or increase the number of types, and removing one can only keep or decrease it. So it's safe. (For sums this is why numbers must be positive. A negative number would break it.)
Frequency maps (dict / Counter)
A frequency map stores value → how many times it appears (in the window). In Python: a dict with freq.get(x, 0) + 1, or collections.Counter / defaultdict(int). len(freq) is the number of distinct values, as long as we delete keys whose count drops to 0.
freq = {}
for x in [1, 1, 2, 1]:
freq[x] = freq.get(x, 0) + 1 # {1: 3, 2: 1} -> 2 distinct types
freq[2] -= 1 # {1: 3, 2: 0}
if freq[2] == 0:
del freq[2] # {1: 3} -> now len(freq) == 1The teacher uses Java's map.getOrDefault(key, 0) + 1, which is the same as Python's freq.get(key, 0) + 1. One more tool, only for Sliding Window Maximum: the monotonic deque, which we don't need here.
The teacher's 4 array patterns
| pattern | when it fits |
|---|---|
| Two pointers | only two particular points matter, nothing in between |
| Sliding window | two points and everything in between matter |
| Prefix sum | continuous sums between two indexes, usually for many queries ("sum from index 2 to 3?") |
| Kadane's | maximum sum when numbers can be negative |
Part A · Brute force: start at every tree, fresh map each time
LeetCode 904
1The question in simple words
A farm has one row of fruit trees, left to right. fruits[i] is the type of fruit on tree i (a number). You want to collect as many fruits as possible, but the owner has rules:
- You have only 2 baskets.
- Each basket holds only one type of fruit, but any amount of it.
- You may start at any tree, then take exactly one fruit from every tree as you walk right. You can't skip a tree and you can't turn back.
- When you reach a tree whose fruit fits neither basket (a third type), you must stop.
Return the most fruits you can pick.
| example | answer | why |
|---|---|---|
| [1, 2, 1] | 3 | 1 goes in basket 1, 2 in basket 2, the next 1 back in basket 1. Only 2 types, so we take all 3. |
| [0, 1, 2, 2] | 3 | [0, 1] is fine, but [0, 1, 2] has 3 types. Best is [1, 2, 2]. |
| [1, 2, 3, 2, 2] | 4 | [2, 3, 2, 2] (the teacher's main example). |
The teacher notices: since the rule is about counting types, a hashmap will probably help. She comes back to that soon.
2What the constraints tell us
- 1 ≤ n ≤ 10⁵. O(n²) would be 10¹⁰, way beyond ~10⁸ → TLE. We'll need O(n). Brute force first anyway.
- 0 ≤ fruits[i] < n. Types go up to about 10⁵, which fits easily in an int. No negatives (that matters later, when she rules out Kadane's).
- n ≥ 1, so there's always at least one tree, and the answer is at least 1.
3Intuition: try every starting tree
Stand at a tree, walk right picking fruit, and keep track of which types are in the baskets. When a third type shows up, stop and note how many fruits you got. Then try again from the next tree. The best of all these tries is the answer.
4Building the logic from examples
Walk it on [1, 2, 3, 2, 2]
Start at index 0: take 1 (basket A), take 2 (basket B) → 2 fruits. Next is 3, a third type → stop. Best so far: 2.
Where do we restart: at the 3 where we got stuck, or at the next tree after the old start?
The teacher asks this on purpose. Restarting at the 3 (index 2) looks tempting, because we stopped there. But then the best we'd find is [3, 2, 2] = 3 fruits. The real best is [2, 3, 2, 2] = 4, which starts at index 1, before the 3. So the brute force must restart at old start + 1, not where it got stuck.
What do we store the baskets in?
"How many different types do I have?" is a question about unique values. For unique values we normally reach for a set or a hashmap. The teacher picks a hashmap (type → count). For the brute force alone, a set would also do (we never take anything out). But the count will be essential once we start removing fruits in Part B, so she uses the map from the start. Her set vs map example is explained in Part B.
Building the inner loop
- Put
fruits[right]in the map: if the type is new, its count becomes 1; if it's there, add 1. (Java'sgetOrDefault, Python'sget(x, 0) + 1.) - Then check: is
len(map) > 2? If yes, this tree is a third type →break(and don't count it). - Otherwise this fruit fits →
count += 1.
After the inner loop ends (by break or by reaching the end), update ans = max(ans, count).
→ Every new start is a new attempt with empty baskets. If the map from the previous start stayed, its old types would block fruits that are actually fine for this start.
→ She first calls the outer pointer
r, then renames it left (it "waits" at the start), and calls the inner one right (it walks and collects). Outer = left = start, inner = right = the tree being picked.5Approach steps
ans = 0.- For every start
left: new empty map,count = 0. - For
rightfromleftto the end: addfruits[right]to the map. - If the map has more than 2 types →
break. Elsecount += 1. - After the inner loop:
ans = max(ans, count). - Return ans.
6Code (Python)
class Solution:
def totalFruit(self, fruits):
n = len(fruits)
ans = 0
for left in range(n): # try every starting tree
basket = {} # fresh baskets: type -> count
count = 0 # fruits picked from this start
for right in range(left, n):
f = fruits[right]
basket[f] = basket.get(f, 0) + 1
if len(basket) > 2: # a third type: must stop
break
count += 1
ans = max(ans, count)
return ans7Code line by line
| line | what it means |
|---|---|
| for left in range(n): | Every tree gets a turn as the starting point. |
| basket = {} count = 0 | New attempt: empty baskets, nothing picked yet. |
| for right in range(left, n): | Walk right from the start, one tree at a time. |
| basket[f] = basket.get(f, 0) + 1 | Put the fruit in: new type starts at 0 + 1, known type goes up by 1. |
| if len(basket) > 2: break | The number of keys = the number of types. More than 2 means this fruit doesn't fit → stop this attempt. We don't count it. |
| count += 1 | The fruit fit, so it's collected. |
| ans = max(ans, count) | After the attempt ends, keep the best total. |
8Dry run (hand table)
fruits = [1, 2, 3, 2, 2].
| start left | trees picked (map after each) | why it stopped | count | ans after |
|---|---|---|---|---|
| 0 | 1 {1:1} → 2 {1:1, 2:1} | 3 makes {1,2,3} → 3 types → break | 2 | 2 |
| 1 | 2 {2:1} → 3 {2:1, 3:1} → 2 {2:2, 3:1} → 2 {2:3, 3:1} | end of array | 4 | 4 |
| 2 | 3 → 2 → 2 | end of array | 3 | 4 |
| 3 | 2 → 2 | end of array | 2 | 4 |
| 4 | 2 | end of array | 1 | 4 |
Final answer: 4 ✓. Notice how start 1 re-added 2 and start 2 re-added 3, 2, 2: the repeated work the sliding window removes.
9Complexity & remember
- Time O(n²): left goes over all n trees, and for every left, right restarts at left and can run nearly to the end. With n = 10⁵ that's ~10¹⁰ → not acceptable.
- Space O(1): the map never holds more than 3 keys.
Part B · Optimal: sliding window with a frequency map
LeetCode 904
1The question again, with the new goal
Same question: the longest run of consecutive trees with at most 2 types. Goal: O(n), with left never restarting.
2Choosing the pattern
- Two pointers ✗: used when only two particular points matter and nothing between them. Here every tree between the two ends matters.
- Prefix sum ✗: for continuous sums between two indexes, typically to answer many range-sum queries. We're not summing anything.
- Kadane's ✗: for maximum sums with negative numbers. The values go from 0 up to 10⁵, no negatives.
- Sliding window ✓: we care about two points and everything between them.
3Intuition: don't restart, just drop trees from the left
Look at [1, 1, 2, 3, 3]. The brute force goes 1, 1, 2, gets stuck at 3, then restarts at index 1 and re-adds 1 and 2, which were already in the map. Why recompute them?
Sliding window's idea instead:
- Expand: keep moving
rightwhile the baskets hold at most 2 types (the condition is true). - When a third type arrives (the condition becomes false), don't throw everything away. Shrink: move
leftforward, removing those fruits from the baskets, until only 2 types are left (the condition is true again). - Then record the length and expand again.
On [1, 1, 2, 3, 3]: when 3 arrives, the window is [1, 1, 2, 3] with 3 types. Drop the first 1 → [1, 2, 3], still 3 types. Drop the second 1 → [2, 3], 2 types ✓. Stop shrinking. Then right picks up the last 3 → [2, 3, 3], length 3.
4Building the logic from examples
Why a hashmap with counts, not a set (her [1, 1, 2, 1, 3] example)
Say the window is [1, 1, 2, 1] and the next tree is 3. Now we must shrink from the left until there are only 2 types.
| remove | with a set {1, 2, 3} | with a map {1:3, 2:1, 3:1} |
|---|---|---|
| first 1 | remove 1 → {2, 3} ✗ but two more 1s are still in the window! | 1's count 3 → 2 → {1:2, 2:1, 3:1}, still 3 types |
| second 1 | (already wrong) | 1's count 2 → 1 → still 3 types |
| the 2 | 2's count 1 → 0 → delete 2 → {1:1, 3:1}, 2 types ✓ |
The window is now [1, 3], and type 1 is still needed (one 1 is still inside). A set can only say "is 1 present: yes/no", so removing one 1 removes the whole type. The map knows how many 1s are inside, so the type only disappears when its count reaches 0. That's why the frequency count is required.
The shrinking step, piece by piece
With a sum we "removed" by subtracting. Here we "added" by increasing a count, so we remove by decreasing it:
basket[fruits[left]] -= 1(one fewer of that type).- If that count is now 0, delete the key: the type is completely gone from the window.
left += 1.
→ We check the number of types with
len(basket), which counts keys, including ones with value 0. A leftover {2: 0} would still count as a type and the window would think it has 3 types when it really has 2.if or while for the shrink?→
while. The teacher first writes an if, then changes it. One removal may not drop a whole type (in the example above it took 3 removals). We keep shrinking until len(basket) ≤ 2 again.→ No, at most two. One type is fine (both baskets aren't required). For [1, 1, 1] the answer is 3, and the code handles it: the map has 1 key, which is never > 2.
Recording the answer
After the while loop, the window is valid again (≤ 2 types), so ans = max(ans, right − left + 1). This is a "longest valid window" problem, so we record after shrinking.
5Approach steps
left = 0,ans = 0, empty mapbasket(made once, before the loop).- For each
right: addfruits[right]to the map (expand). - While the map has more than 2 types: decrease the count of
fruits[left], delete it if 0,left += 1(shrink). ans = max(ans, right − left + 1).- Return ans.
6Code (Python)
class Solution:
def totalFruit(self, fruits):
basket = {} # type -> how many in the window
left = 0
ans = 0
for right in range(len(fruits)):
f = fruits[right]
basket[f] = basket.get(f, 0) + 1 # expand: pick this fruit
while len(basket) > 2: # third type: shrink from the left
g = fruits[left]
basket[g] -= 1
if basket[g] == 0:
del basket[g] # type completely gone
left += 1
ans = max(ans, right - left + 1) # window is valid again
return ans7Code line by line
| line | what it means |
|---|---|
| basket = {} left = 0 ans = 0 | The map is created once, outside the loop. Unlike the brute force, it is never reset: it always describes the current window. |
| for right in range(len(fruits)): | The expansion pointer visits every tree once. |
| basket[f] = basket.get(f, 0) + 1 | Add the new fruit to the window's counts. |
| while len(basket) > 2: | More than 2 types in the window → invalid → keep shrinking. |
| basket[g] -= 1 | The leftmost fruit leaves the window: one fewer of its type. |
| if basket[g] == 0: del basket[g] | No more of that type inside, so remove the key, otherwise len would still count it. |
| left += 1 | The window now starts one tree later. |
| ans = max(ans, right - left + 1) | The window is valid. Its length is the number of fruits picked. Keep the biggest. |
8Dry run
fruits = [1, 2, 3, 2, 2] (the teacher's example).
| step | right (value added) | window [left..right] | map after adding | shrink? (what leaves, why) | window after shrinking | ans so far |
|---|---|---|---|---|---|---|
| 1 | 0 (1) | [1] (0..0) | {1:1} | no, 1 type | [1] (0..0) | 1 |
| 2 | 1 (2) | [1,2] (0..1) | {1:1, 2:1} | no, 2 types | [1,2] (0..1) | 2 |
| 3 | 2 (3) | [1,2,3] (0..2) | {1:1, 2:1, 3:1} | yes, 3 types: 1 leaves (count 0 → delete) → {2:1, 3:1} | [2,3] (1..2) | 2 |
| 4 | 3 (2) | [2,3,2] (1..3) | {2:2, 3:1} | no, 2 types | [2,3,2] (1..3) | 3 |
| 5 | 4 (2) | [2,3,2,2] (1..4) | {2:3, 3:1} | no, 2 types | [2,3,2,2] (1..4) | 4 |
The array at the key moments (yellow = window, grey = left behind):
Final answer: 4 ✓, matching the brute force. In step 3 the window shrank by one tree and kept 2 and 3. The brute force would have thrown everything away and started again.
A second picture, her [1, 1, 2, 1, 3] example, where the shrink needs three removals:
For [1, 1, 2, 1, 3] the best answer is 4 ([1, 1, 2, 1]), recorded at step 4, before the 3 arrived.
9Complexity & remember
- Time O(n): right moves from 0 to n − 1 once. The
whileinside looks like it makes it O(n²), butleftnever goes back to the start. It also moves forward only, touching each index at most once in the whole run. So the total is n + n = O(2n) = O(n). Each map operation is O(1). - Space O(1): the map holds at most 3 types at any moment (2, plus the one that triggers a shrink).
She shows the final code accepted as one of the fastest solutions.
basket[f] += 1 → while len(basket) > 2: decrement fruits[left], delete at 0, left += 1 → ans = max(ans, right − left + 1).Counts (not a set), because a type may still be in the window after one copy leaves.
Part C · Revision page
| Brute force | Sliding window | |
|---|---|---|
| idea | for every start, walk right until a 3rd type, keep the best count | expand right, and when a 3rd type appears, shrink left until 2 types remain |
| map | new map for every start | one map for the whole run, always = current window |
| after getting stuck | restart at start + 1 (re-adding fruits) | drop fruits from the left only |
| time / space | O(n²) / O(1) | O(n) (≈2n moves) / O(1) |
| pattern | why it does / doesn't fit |
|---|---|
| Two pointers | only two points matter there; here every tree between them matters |
| Prefix sum | for range sums and queries; nothing is summed here |
| Kadane's | max sum with negatives; values are 0…10⁵ |
| Sliding window | contiguous trees, rule "≤ 2 types" only gets worse when growing and better when shrinking |
2. Window summary = frequency map (type → count).
len(map) = number of types.3. Expand: count up. While more than 2 types: count down
fruits[left], delete at 0, move left.4. Record
max(ans, right − left + 1) after shrinking.5. left never restarts → O(n).
✗ forgetting to delete a key whose count is 0 (
len still counts it)✗
if instead of while for shrinking✗ reading it as "exactly 2 types" (1 type is fine)
✗ in the brute force, restarting where you got stuck instead of at start + 1
✗ in the brute force, not resetting the map for each new start
s = Solution() print(s.totalFruit([1, 2, 1])) # 3 print(s.totalFruit([0, 1, 2, 2])) # 3 print(s.totalFruit([1, 2, 3, 2, 2])) # 4 print(s.totalFruit([3, 3, 3, 1, 2, 1, 1, 2, 3, 3, 4])) # 5 print(s.totalFruit([1, 1, 1])) # 3 (one type is fine) print(s.totalFruit([0, 1, 2, 3])) # 2 (all different) print(s.totalFruit([7])) # 1
Based on this video: Fruits Into Baskets | Sliding Window