LC 875 — First Principles

Koko Eating Bananas — build the intuition before the optimization

Start from the brute-force thought process, understand exactly what work is repeated, and then earn the binary-search optimization instead of memorizing it.

Search on Answer Brute force to optimized Same style as the main binary-search guide

What are we actually minimizing?

Problem Statement

We need the minimum integer eating speed k such that Koko can finish all banana piles within h hours.

Input:piles = [3, 6, 7, 11], h = 8
Goal:Find the smallest speed k that lets her finish in time

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.

1
Pick a candidate speed.

Start with k = 1, then 2, then 3, and so on.

2
Compute how many hours that speed needs.

For one pile of size pile, hours needed is ceil(pile / k).

3
Return the first speed that works.

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

Example
piles[3, 6, 7, 11]
h8

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?

Brute-Force Cost
Candidate speeds
1 .. maxPile

If the largest pile is huge, there are many possible speeds to test.

Work per speed
O(n)

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:

canFinish(speed) = totalHours(speed) <= h ?

That repeated feasibility check is the heart of the problem. The only thing changing is the candidate speed.

1
Choose a speed.

speed = 1, then 2, then 3, then 4...

2
Recompute hours across all piles.

The full pile scan happens again and again for each speed candidate.

3
Check true or false.

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.

For piles = [3, 6, 7, 11], h = 8
k=1F
k=2F
k=3F
k=4T
k=5T
k=6T
k=7T
k=8T
This is the classic binary-search shape: false, false, false, then true forever.

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?

Brute force
O(n * maxPile)

Checks every possible speed one by one until it finds the first valid one.

Binary search
O(n log maxPile)

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:

Do the check results form F F F ... T T T ?

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.