Canonical Deep-Dive: Koko Eating Bananas
Problem: Koko Eating Bananas (https://leetcode.com/problems/koko-eating-bananas/) The lesson: binary search does not need a sorted array. It needs a monotonic yes-or-no question, and here you build one out of thin air.
This is a guided walk-through, not an answer key. It explains how to think about searching an answer space instead of an array: the search space, the feasible predicate, why that predicate is monotonic, and the exact complexity. It stops short of an assembled, paste-runnable function on purpose. You write the actual Python yourself, in your own work repo. The tutor will not confirm your code; LeetCode’s judge will.
Restated: Koko has piles of bananas, where piles[i] is the number of bananas in the i-th pile. The guards come back in h hours. Each hour Koko picks one pile and eats up to k bananas from it; if the pile has fewer than k left, she eats it all and that hour is over (she does not move on to another pile within the same hour). Find the smallest integer eating speed k such that she finishes every pile within h hours. You are guaranteed h >= len(piles), so a finishing speed always exists.
Why the obvious idea is too slow (and what it hides)
The first instinct is to try speeds one at a time: start at k = 1, compute how many hours that takes, and increase k until it fits in h. That works, and it is worth holding in your head as the baseline, because the structure of that loop is the whole insight in disguise.
- Time: O(
max(piles)* n). For each candidate speed up tomax(piles)you scan all n piles to total the hours. With piles up to a billion, that is far too slow. - Space: O(1).
But look at what that loop is really doing. It scans candidate speeds 1, 2, 3, ... in increasing order and stops at the first one that fits. It is a linear search for a threshold in a sorted list of candidate speeds. Anything that is a linear search for a threshold in a sorted space is a binary search waiting to happen. The speeds are already ordered (they are just the integers from 1 to max(piles)); we do not need to sort anything. We only need to confirm that “does speed k fit?” behaves monotonically, and then we can binary search for the boundary instead of walking to it.
The brute-force baseline you actually keep: the hours function
Before optimizing the search, nail down the one piece of arithmetic both versions need: given a speed k, how many hours does Koko take?
For a single pile of size p eaten at speed k, she needs ceil(p / k) hours: full hours of k bananas each, plus one more partial hour for the remainder. A pile of 7 at speed 3 takes ceil(7/3) = 3 hours (3 + 3 + 1). The total hours for a speed is the sum over all piles:
hours(k) = sum( ceil(piles[i] / k) for each pile i )
Compute this with math.ceil(p / k), or with the integer-only form (p + k - 1) // k if you want to avoid floats entirely (know both; the integer form sidesteps any floating-point rounding worry). This function is O(n), one pass over the piles, and it is the per-candidate cost in both the brute force and the binary search.
The four-step framework: binary search on the answer
This is the recipe for the whole week. Walk it in order, every time the input is not a plain sorted array.
1. Identify the answer space. What single number are you choosing, and what are its bounds?
The answer is the eating speed
k. The slowest sensible speed is1(she must eat at least one banana an hour to ever finish). The fastest speed worth considering ismax(piles): eating faster than the largest pile cannot help, because each hour is capped at one pile anyway, so a speed ofmax(piles)already finishes every pile in exactly one hour each. Solo = 1andhi = max(piles).
Say those bounds out loud and justify the upper one. A speed above max(piles) would finish in len(piles) hours just like max(piles) does; it buys nothing, so there is no reason to search past it.
2. Write feasible(k). A yes-or-no test on a candidate answer.
feasible(k)isTrueif Koko finishes within the deadline at speedk, that is, ifhours(k) <= h, andFalseotherwise.
That is the entire predicate: total the hours at speed k using the function above, and compare to h. Nothing about this step searches; it just answers one question about one candidate.
3. Confirm monotonicity, and say why. This is the step people skip, and skipping it is the usual way these solutions come out wrong. The method is only valid if feasible flips exactly once as k increases.
As
kgrows,hours(k)never increases: eating faster takes the same number of hours or fewer for every pile, sinceceil(p / k)is non-increasing ink. So once some speedk0is fast enough (hours(k0) <= h), every speed faster thank0is also fast enough. The predicate isFalse, False, ..., False, True, True, ..., Truewith a single flip. That single flip is exactly what binary search locates.
Do not take this on faith because the examples pass. Argue it: faster speed, fewer (or equal) hours, therefore “feasible” can only turn on once and never turn back off.
4. Binary search the boundary. You want the smallest k for which feasible(k) is True, because the problem asks for the minimum speed. Use the half-open template that converges lo and hi onto that boundary:
- While
lo < hi: takemid = (lo + hi) // 2. Iffeasible(mid), the answer ismidor smaller, so keep the left half includingmid:hi = mid. Otherwisemidis too slow, so discard it and everything below:lo = mid + 1. - When
lo == hi, that single index is the smallest feasible speed. Return it.
Notice the invariant: the range [lo, hi] always contains the smallest feasible speed, and each step halves it. The loop ends because the range strictly shrinks every iteration. Because a finishing speed is guaranteed to exist (the problem promises h >= len(piles)), hi = max(piles) is itself feasible, so the search never falls off the top.
The key insight, in one sentence
The list of candidate speeds is already sorted (it is just the integers from 1 to max(piles)), and “finishes in time” is a property that, once true, stays true as the speed rises, so finding the slowest acceptable speed is a binary search for the single point where an unsorted-looking problem’s hidden monotonic predicate flips from False to True. You never sort anything and you never build the list of speeds; you only ever evaluate feasible at a midpoint and throw away half the range.
Exact complexity
- Time: O(n log(
max(piles))). The binary search runs O(log(max(piles))) iterations, because each iteration halves a speed range that starts at widthmax(piles). Each iteration callsfeasible, which is O(n) (one pass to total the hours over the n piles). Multiply them: O(n log(max(piles))). Note thelogis of the largest pile, the size of the answer space, not ofn; that is the tell of “binary search on the answer”. - Space: O(1). You hold a handful of integers (
lo,hi,mid, a running hour total). The piles are the input and do not count; nothing extra grows with the input.
A worked check (do this by hand before you code)
Take piles = [3, 6, 7, 11], h = 8. The answer space is [1, 11]. Sanity-check the endpoints and the answer with the hours function:
- Speed
3:ceil(3/3) + ceil(6/3) + ceil(7/3) + ceil(11/3) = 1 + 2 + 3 + 4 = 10hours. That is more than8, so speed3is infeasible. - Speed
4:ceil(3/4) + ceil(6/4) + ceil(7/4) + ceil(11/4) = 1 + 2 + 2 + 3 = 8hours. That is exactly8, so speed4is feasible.
So the predicate is False at 3 and True at 4: the flip is between them, and the smallest feasible speed is 4. Confirm that your binary search converges to 4 on this input, and that it gives 30 for piles = [30, 11, 23, 4, 20], h = 5 and 23 for the same piles with h = 6. Working these by hand first is how you catch a boundary bug before the judge does.
Your work this week
- Implement Koko Eating Bananas yourself, in your own work repo (for example
koko-eating-bananas.py). Write thehours(k)helper first and test it in isolation on the worked example above; then wrap it in the binary search over[1, max(piles)]. Build it from the four steps; do not copy a solution. - Write the
feasiblepredicate as its own named function or inline helper, not as a tangle inside the loop. The clarity offeasibleis what makes the monotonicity argument obvious, and an interviewer will ask you to state that argument. - Run it against the worked examples by hand and in code, then submit to LeetCode’s judge. Test the edges yourself: a single pile (the answer is
ceil(pile / 1)constrained byh), all piles equal, andhexactly equal tolen(piles)(which forces the fastest speed,max(piles)). - Commit with a five-question debrief in the message. In answer 3, state the exact complexity above and, crucially, say what the
logis the log of and why it ismax(piles)and notn.
When you can explain, from memory and with no AI open, the four steps for this problem (answer space, feasible, why it is monotonic, search the boundary) and why a faster speed can never increase the hours, you own this problem. That explanation is exactly what an interviewer will ask for, and it is the template you will reuse on every “binary search on the answer” problem for the rest of your career.