What are we actually minimizing?
We need the minimum integer eating speed k such that Koko can finish all banana piles within h hours.
The important subtlety: the answer is not inside the array. We are not looking for a target element. We are looking for the smallest speed that satisfies a constraint.
If we knew nothing about binary search, what would we do?
The most natural idea is to try every possible speed, starting from the slowest.
Start with k = 1, then 2, then 3, and so on.
For one pile of size pile, hours needed is ceil(pile / k).
Because we are trying speeds in increasing order, the first valid one must be the minimum answer.
The brute-force algorithm is simply: test every speed from 1 to max(piles), and stop at the first one that finishes within h hours.
The naive solution is completely reasonable
class Solution { public int minEatingSpeed(int[] piles, int h) { int maxPile = 0; for (int pile : piles) { maxPile = Math.max(maxPile, pile); } for (int speed = 1; speed <= maxPile; speed++) { int hoursNeeded = 0; for (int pile : piles) { hoursNeeded += (pile + speed - 1) / speed; } if (hoursNeeded <= h) { return speed; } } return maxPile; } }
class Solution: def minEatingSpeed(self, piles: list[int], h: int) -> int: max_pile = max(piles) for speed in range(1, max_pile + 1): hours_needed = 0 for pile in piles: hours_needed += (pile + speed - 1) // speed if hours_needed <= h: return speed return max_pile
If you are wondering about hoursNeeded += (pile + speed - 1) / speed;: this is integer-math shorthand for ceil(pile / speed). Example: if pile = 10 and speed = 3, plain integer division gives 10 / 3 = 3, but Koko actually needs 4 hours. The expression (10 + 3 - 1) / 3 = 12 / 3 = 4 forces the division to round up.
This code is not a hack. It reflects the direct reasoning process. We are literally asking: what is the smallest speed that works?
Run the brute force with real values
Try speed = 1
Hours needed = 3 + 6 + 7 + 11 = 27. Too slow.
Try speed = 2
Hours needed = ceil(3/2) + ceil(6/2) + ceil(7/2) + ceil(11/2) = 2 + 3 + 4 + 6 = 15. Still too slow.
Try speed = 3
Hours needed = 1 + 2 + 3 + 4 = 10. Still too slow.
Try speed = 4
Hours needed = 1 + 2 + 2 + 3 = 8. This works, so we return 4.
Why return on hoursNeeded <= h? Because the problem says Koko must finish within h hours. Equal is valid, smaller is also valid.
Why does brute force get expensive?
If the largest pile is huge, there are many possible speeds to test.
Each speed requires scanning every pile to compute total hours.
That gives us:
Time = O(n * maxPile) Space = O(1)
So if maxPile is large, brute force wastes time checking lots of impossible speeds one by one.
What work are we repeating?
For every candidate speed, we ask the same yes/no question:
That repeated feasibility check is the heart of the problem. The only thing changing is the candidate speed.
speed = 1, then 2, then 3, then 4...
The full pile scan happens again and again for each speed candidate.
Does this speed finish within h hours or not?
We are not optimizing the formula for hours. We are optimizing how many candidate speeds we need to test.
The answers form a monotonic map
If a speed works, then every faster speed also works. If a speed fails, every slower speed also fails.
This is the key shift: the piles are not sorted, but the answer space is monotonic. That is what binary search needs.
Optimize the search over speeds
Once we notice the false-to-true transition, we stop testing every speed one by one. Instead, we binary search the speed range 1 .. maxPile and ask the same feasibility question only for the middle candidates.
class Solution { public int minEatingSpeed(int[] piles, int h) { int left = 1; int right = 0; for (int pile : piles) { right = Math.max(right, pile); } int answer = right; while (left <= right) { int mid = left + (right - left) / 2; if (canFinish(piles, mid, h)) { answer = mid; right = mid - 1; } else { left = mid + 1; } } return answer; } private boolean canFinish(int[] piles, int speed, int h) { int hoursNeeded = 0; for (int pile : piles) { hoursNeeded += (pile + speed - 1) / speed; } return hoursNeeded <= h; } }
class Solution: def minEatingSpeed(self, piles: list[int], h: int) -> int: left, right = 1, max(piles) answer = right while left <= right: mid = left + (right - left) // 2 if self.can_finish(piles, mid, h): answer = mid right = mid - 1 else: left = mid + 1 return answer def can_finish(self, piles, speed, h): hours_needed = 0 for pile in piles: hours_needed += (pile + speed - 1) // speed return hours_needed <= h
We still compute hoursNeeded the same way. The optimization is that we test only log maxPile candidate speeds instead of every speed from 1 to maxPile.
What exactly improved?
Checks every possible speed one by one until it finds the first valid one.
Keeps the same canFinish(speed) check, but cuts the candidate-speed range in half each step.
The repeated work is still there, but binary search massively reduces how many times we perform it.
The reusable lesson
Whenever your brute-force approach looks like this:
for every possible answer:
check if this answer works
ask one follow-up question:
If yes, you likely have a binary-search-on-answer problem.
For Koko: the formula did not change, the feasibility check did not change, only the way we searched the candidate answers changed.