200 lines
9.2 KiB
YAML
200 lines
9.2 KiB
YAML
title: Koko Eating Bananas
|
||
slug: koko-eating-bananas
|
||
difficulty: medium
|
||
leetcode_id: 875
|
||
leetcode_url: https://leetcode.com/problems/koko-eating-bananas/
|
||
categories:
|
||
- arrays
|
||
- binary-search
|
||
patterns:
|
||
- slug: binary-search
|
||
is_optimal: true
|
||
|
||
function_signature: "def min_eating_speed(piles: list[int], h: int) -> int:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { piles: [3, 6, 7, 11], h: 8 }
|
||
expected: 4
|
||
- input: { piles: [30, 11, 23, 4, 20], h: 5 }
|
||
expected: 30
|
||
- input: { piles: [30, 11, 23, 4, 20], h: 6 }
|
||
expected: 23
|
||
hidden:
|
||
- input: { piles: [1], h: 1 }
|
||
expected: 1
|
||
- input: { piles: [1, 1, 1, 1], h: 4 }
|
||
expected: 1
|
||
- input: { piles: [1000000000], h: 2 }
|
||
expected: 500000000
|
||
- input: { piles: [312884470], h: 968709470 }
|
||
expected: 1
|
||
- input: { piles: [2, 2], h: 2 }
|
||
expected: 2
|
||
- input: { piles: [5, 10, 3], h: 4 }
|
||
expected: 6
|
||
|
||
description: |
|
||
Koko loves to eat bananas. There are `n` piles of bananas, the i<sup>th</sup> 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 less 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*.
|
||
|
||
constraints: |
|
||
- `1 <= piles.length <= 10^4`
|
||
- `piles.length <= h <= 10^9`
|
||
- `1 <= piles[i] <= 10^9`
|
||
|
||
examples:
|
||
- input: "piles = [3,6,7,11], h = 8"
|
||
output: "4"
|
||
explanation: "At speed k = 4, Koko takes ceil(3/4) + ceil(6/4) + ceil(7/4) + ceil(11/4) = 1 + 2 + 2 + 3 = 8 hours, which exactly meets the deadline."
|
||
- input: "piles = [30,11,23,4,20], h = 5"
|
||
output: "30"
|
||
explanation: "With only 5 hours and 5 piles, Koko must finish each pile in exactly 1 hour. She needs k = 30 (the largest pile) to eat any pile in one hour."
|
||
- input: "piles = [30,11,23,4,20], h = 6"
|
||
output: "23"
|
||
explanation: "With 6 hours for 5 piles, Koko has one extra hour. At k = 23, the pile of 30 takes ceil(30/23) = 2 hours, while all others take 1 hour each, totaling 6 hours."
|
||
|
||
explanation:
|
||
intuition: |
|
||
Imagine you're given a dial that controls Koko's eating speed. Turn it up, and she finishes faster but eats more per hour than necessary. Turn it down, and she enjoys smaller bites but risks running out of time.
|
||
|
||
The key insight is that this dial creates a **monotonic relationship**: if Koko can finish at speed `k`, she can definitely finish at any speed greater than `k`. Conversely, if she can't finish at speed `k`, she can't finish at any slower speed either.
|
||
|
||
This monotonicity is the hallmark of a **binary search on the answer** problem. Instead of searching for an element in an array, we're searching for the minimum valid value in a range of possible speeds.
|
||
|
||
Think of it like this: imagine all possible speeds from `1` to `max(piles)` laid out on a number line. At some point, there's a boundary — speeds below it fail (too slow), and speeds at or above it succeed. Binary search efficiently finds that boundary.
|
||
|
||
approach: |
|
||
We use **Binary Search on the Answer** to find the minimum valid eating speed:
|
||
|
||
**Step 1: Define the search space**
|
||
|
||
- `left`: Set to `1` — the minimum possible speed (eating at least one banana per hour)
|
||
- `right`: Set to `max(piles)` — eating faster than the largest pile is wasteful since Koko can only eat from one pile per hour
|
||
|
||
|
||
|
||
**Step 2: Implement the feasibility check**
|
||
|
||
- For a given speed `k`, calculate the total hours needed to eat all piles
|
||
- For each pile, the hours needed is `ceil(pile / k)`, which equals `(pile + k - 1) // k` using integer math
|
||
- If total hours `<= h`, the speed is feasible
|
||
|
||
|
||
|
||
**Step 3: Binary search for the minimum valid speed**
|
||
|
||
- Calculate `mid = (left + right) // 2`
|
||
- If `mid` is a feasible speed, it might be our answer, but there could be a smaller valid speed — search left by setting `right = mid`
|
||
- If `mid` is not feasible, we need a faster speed — search right by setting `left = mid + 1`
|
||
- Continue until `left == right`
|
||
|
||
|
||
|
||
**Step 4: Return the result**
|
||
|
||
- Return `left` (or `right`) as the minimum speed that allows Koko to finish on time
|
||
|
||
common_pitfalls:
|
||
- title: Trying All Speeds Linearly
|
||
description: |
|
||
A naive approach checks every speed from `1` to `max(piles)` and returns the first one that works.
|
||
|
||
With `max(piles)` up to `10^9`, this linear search performs up to a billion iterations — far too slow.
|
||
|
||
Binary search reduces this to at most `log2(10^9) ≈ 30` iterations.
|
||
wrong_approach: "Linear search from 1 to max(piles)"
|
||
correct_approach: "Binary search on the speed range"
|
||
|
||
- title: Incorrect Ceiling Division
|
||
description: |
|
||
When calculating hours for a pile, we need `ceil(pile / k)`. Using regular integer division `pile // k` gives the floor, which undercounts.
|
||
|
||
For example, `pile = 7, k = 4`: floor is `1`, but Koko actually needs `2` hours (one hour for 4 bananas, another for the remaining 3).
|
||
|
||
Use the formula `(pile + k - 1) // k` or Python's `math.ceil(pile / k)` for correct results.
|
||
wrong_approach: "pile // k (floor division)"
|
||
correct_approach: "(pile + k - 1) // k (ceiling division)"
|
||
|
||
- title: Wrong Search Space Bounds
|
||
description: |
|
||
Setting `right` too high (e.g., `sum(piles)` or `h`) wastes iterations. The maximum useful speed is `max(piles)` because eating faster doesn't help — Koko still spends one hour per pile regardless.
|
||
|
||
Setting `left` to `0` causes division by zero errors. The minimum meaningful speed is `1`.
|
||
wrong_approach: "left = 0 or right = sum(piles)"
|
||
correct_approach: "left = 1, right = max(piles)"
|
||
|
||
- title: Off-by-One in Binary Search
|
||
description: |
|
||
When searching for a minimum valid value:
|
||
- If `mid` works, set `right = mid` (not `mid - 1`) because `mid` could be the answer
|
||
- If `mid` fails, set `left = mid + 1`
|
||
|
||
Using `right = mid - 1` when `mid` is valid might skip the answer. The loop condition `left < right` ensures we converge correctly.
|
||
wrong_approach: "right = mid - 1 when mid is feasible"
|
||
correct_approach: "right = mid when mid is feasible"
|
||
|
||
key_takeaways:
|
||
- "**Binary search on the answer**: When asked to find the minimum/maximum value satisfying a condition, and the condition is monotonic, binary search applies"
|
||
- "**Monotonicity is key**: If a speed `k` works, all larger speeds work too — this sorted property enables binary search"
|
||
- "**Ceiling division pattern**: `(a + b - 1) // b` computes `ceil(a / b)` using only integers, avoiding floating-point issues"
|
||
- "**Similar problems**: This pattern applies to Capacity To Ship Packages Within D Days, Split Array Largest Sum, and Magnetic Force Between Two Balls"
|
||
|
||
time_complexity: "O(n log m). Binary search runs `O(log m)` iterations where `m = max(piles)`, and each feasibility check scans all `n` piles."
|
||
space_complexity: "O(1). We only use a constant number of variables for the search bounds and hour calculations."
|
||
|
||
solutions:
|
||
- approach_name: Binary Search on Answer
|
||
is_optimal: true
|
||
code: |
|
||
def min_eating_speed(piles: list[int], h: int) -> int:
|
||
# Search space: minimum speed 1, maximum speed is largest pile
|
||
left, right = 1, max(piles)
|
||
|
||
while left < right:
|
||
mid = (left + right) // 2
|
||
|
||
# Calculate total hours needed at speed mid
|
||
hours_needed = sum((pile + mid - 1) // mid for pile in piles)
|
||
|
||
if hours_needed <= h:
|
||
# Speed mid works, but maybe we can go slower
|
||
right = mid
|
||
else:
|
||
# Too slow, need to eat faster
|
||
left = mid + 1
|
||
|
||
return left
|
||
explanation: |
|
||
**Time Complexity:** O(n log m) — Binary search over `m = max(piles)` speeds, each iteration scans `n` piles.
|
||
|
||
**Space Complexity:** O(1) — Only constant extra space used.
|
||
|
||
We binary search for the minimum speed where Koko can finish on time. The feasibility check sums up the hours needed for each pile using ceiling division.
|
||
|
||
- approach_name: Linear Search
|
||
is_optimal: false
|
||
code: |
|
||
def min_eating_speed(piles: list[int], h: int) -> int:
|
||
# Try every speed from 1 up to max pile
|
||
for k in range(1, max(piles) + 1):
|
||
# Calculate hours needed at this speed
|
||
hours_needed = sum((pile + k - 1) // k for pile in piles)
|
||
|
||
# Return first speed that works
|
||
if hours_needed <= h:
|
||
return k
|
||
|
||
return max(piles)
|
||
explanation: |
|
||
**Time Complexity:** O(n × m) — Checks up to `m = max(piles)` speeds, each requiring O(n) time.
|
||
|
||
**Space Complexity:** O(1) — Only constant extra space used.
|
||
|
||
This brute force approach tries every possible speed starting from 1. While correct, it times out on large inputs where `max(piles)` can be up to `10^9`. Included to illustrate why binary search is essential.
|