Koko Eating Bananas
“Eliminate half the search space every step. You don't search — you shrink until only one answer remains.”
The Problem
Koko loves to eat bananas. There are n piles of bananas, the ith pile has piles[i] bananas. The guards have gone and will come back in h hours.
Koko can decide her bananas-per-hour eating speed of k. Each hour, she chooses some pile of bananas and eats k bananas from that pile. If the pile has fewer than k bananas, she eats all of them instead, and will not eat any more bananas during that hour.
Koko likes to eat slowly but still wants to finish eating all the bananas before the guards return.
Return the minimum integer k such that she can eat all the bananas within h hours.
Example 1:
Input: piles = [3,6,7,11], h = 8
Output: 4
Example 2:
Input: piles = [30,11,23,4,20], h = 5
Output: 30
Example 3:
Input: piles = [30,11,23,4,20], h = 6
Output: 23
The Analogy: The Guard's Deadline
What are we actually searching for?
Koko wants to eat as slowly as possible, but she has a hard deadline. She is not looking for a speed that was decided in advance — she is asking: what is the minimum speed where I still finish before the guards return?
That reframes the problem. The question is not "does speed 7 work?" It is "where does 'works' first become true?" Once you see it that way, you are not solving a calculation problem. You are searching for a boundary.
The Speed Dial
Think of every possible eating speed as a dial with numbered notches. Turn it all the way left and Koko barely eats anything per hour — she will never finish in time. Turn it all the way right and she tears through every pile as fast as possible.
Somewhere on that dial there is a boundary. Every notch to the left of it is too slow. Every notch at or to the right of it is fast enough. Your job is to find that boundary — specifically the leftmost notch that still works.
How we define the range
Each pile has a floor: one hour. Koko works on one pile per hour and cannot do less than that, no matter how fast she eats.
The bigger a pile is, the faster she needs to eat to clear it in that one hour. A small pile hits its floor at a low speed. A large pile needs a higher speed to get there. The biggest pile is the hardest — it needs the highest speed. But once her speed reaches the size of that pile, every smaller pile already fits in one hour too.
That is why max(piles) is the ceiling. It is the speed at which every pile — including the hardest one — has hit its floor. Searching beyond it changes nothing.
So the range we search is every candidate speed from 1 to max(piles). That range is just a sorted list of integers — not the pile array. piles never gets searched. It only appears when we evaluate a candidate speed to see how many total hours it would take.
The one flip that makes binary search valid
Every speed that works has one property: every faster speed also works. Every speed that fails has one property: every slower speed also fails. The boundary between them flips exactly once — from "too slow" to "fast enough" — and never flips back.
That single flip is what makes Binary Search the right tool. You do not need to test every notch. You probe the middle of the remaining range, check whether it finishes in time, and cut the range in half. The boundary always survives in one half or the other.
Testing a speed
To test any candidate speed, you walk every pile and calculate how many hours that pile would take at that speed. A pile rarely divides evenly — any leftover bananas still cost a full hour, so you always round up. Sum those hours across every pile. If the total fits within h, the speed works. If not, it fails. That check is all you need to decide which half of the dial to keep searching.
How I Think Through This
I picture Koko starting in the middle of the dial rather than the slow end. I probe that midpoint, total the hours across every pile, and ask: does this fit the deadline?
If it does, I do not stop — there might be a slower notch that also works. I squeeze left. If it does not, I move right. Each probe cuts the remaining dial in half.
When the two cursors cross, left is sitting on the first notch that survived every test. That is the answer.
Take piles = [3, 6, 7, 11], h = 8.
Building the Algorithm
Step 1: Build the Hours Helper
Before you can probe any notch on the dial, you need a way to test it. Write canFinishAtSpeed — walk each pile in piles, figure out how many hours that pile costs at speed k, and return whether the total stays within h.
Why Math.ceil(pile / k)? Koko works on one pile per hour and any leftover capacity in that hour is gone. A pile of 7 bananas at speed 3 takes 3 hours, not 2.33 — the partial third sitting still costs a full hour. Rounding up captures that.
Hints & gotchas
- Partial sittings still cost a full hour: a pile with 7 bananas at speed 3 costs 3 hours, not 2.33.
- One pile per iteration: sum the hour cost across every pile, not just the largest.
- Return a boolean: the binary search only needs to know if this speed works, not the raw hour count.
Step 2: Find a Working Speed
Now set up the dial. The left edge is 1 — the slowest speed that makes any progress. The right edge is max(piles) — at that speed, Koko clears the biggest pile in exactly one hour, so going faster would not reduce the total hour count any further. Those two ends define the only notches worth searching.
Set up a Binary Search over that range, probe the midpoint, and call canFinishAtSpeed. If the midpoint works, return it. For now, don't worry about whether it is the minimum.
Take piles = [8, 8, 8, 8], h = 8.
Hints & gotchas
- The midpoint is a speed, not an index: search from
1tomax(piles). - Use your helper: call
canFinishAtSpeed(piles, h, mid)to test the midpoint. - Just return mid for now: the goal is to confirm the binary search structure works before adding the squeeze logic.
Step 3: Squeeze to the Minimum Working Speed
Finding a working speed is not the goal — finding the minimum working speed is. When a notch works, you cannot stop; there might be a slower one that also works. Replace return mid with right = mid - 1 to keep squeezing left. Fill in the failing branch with left = mid + 1. After the loop, left is sitting on the leftmost notch that survived every test — the boundary you were hunting for.
Take piles = [30, 11, 23, 4, 20], h = 6.
Hints & gotchas
- If a speed works, search left: you are hunting for the slowest setting that still finishes on time.
- If a speed fails, search right: every slower speed also fails, so there is nothing to keep on the left.
leftbecomes the answer: once the boundaries cross, every smaller speed has been disproved.
Tracing through an Example
Take piles = [25, 10, 23, 4], h = 7.
Speed Dial at a Glance
Recognizing This Pattern
Reach for binary search on an answer space when three things are true:
- The problem asks for a minimum or maximum value, not a position in an array.
- That value lives in a natural sorted range — here, every integer from
1tomax(piles). - There is a monotonic condition: if speed
kworks, thenk + 1also works; ifkfails,k - 1also fails. One flip, never two.
The tell is when you catch yourself thinking "I need to check every possible value" — that is the brute-force instinct. When checking one candidate is cheap (one pass through piles) but the range is large, binary search on that range collapses the work from O(n · max) to O(n · log max).