875. Koko Eating Bananas
Problem Description
Koko has n
piles of bananas in front of her, where the i-th
pile contains piles[i]
bananas. She needs to eat all the bananas before the guards return in h
hours.
Koko eats bananas at a constant speed of k
bananas per hour. During each hour, she follows these rules:
- She picks one pile to eat from
- If the pile has
k
or more bananas, she eats exactlyk
bananas from it - If the pile has fewer than
k
bananas, she eats all remaining bananas in that pile and doesn't eat any more bananas during that hour (even if there's time left)
The goal is to find the minimum eating speed k
(bananas per hour) that allows Koko to finish all bananas within h
hours.
For example, if piles = [3, 6, 7, 11]
and h = 8
:
- If
k = 4
, Koko needs: 1 hour for pile 1 (3 bananas), 2 hours for pile 2 (6 bananas), 2 hours for pile 3 (7 bananas), and 3 hours for pile 4 (11 bananas), totaling 8 hours - If
k = 3
, she would need 10 hours total, which exceeds the limit
The problem uses binary search because eating speeds have a monotonic property: if Koko can finish at speed k
, she can also finish at any speed greater than k
. The solution searches for the minimum valid speed between 1 and max(piles)
using the helper function check(k)
which calculates whether all bananas can be eaten within h
hours at speed k
. The expression (x + k - 1) // k
computes the ceiling division to determine hours needed for each pile.
Intuition
The key insight is recognizing that this problem has a monotonic property: if Koko can eat all bananas at speed k
within h
hours, then she can definitely eat them at any speed faster than k
. Conversely, if she cannot finish at speed k
, then she cannot finish at any speed slower than k
either.
This monotonic behavior immediately suggests binary search as a solution strategy. We need to find the minimum value of k
that satisfies our constraint - a classic "find the leftmost valid value" binary search problem.
The search space for k
is bounded. The minimum possible speed is 1
(eating one banana per hour), and the maximum useful speed is max(piles)
- eating any faster than the largest pile size provides no benefit since Koko can only eat from one pile per hour anyway.
For any given speed k
, we can calculate how many hours Koko needs to finish all piles. For a pile with x
bananas:
- If
x
is divisible byk
, she needs exactlyx / k
hours - If not, she needs
x // k + 1
hours (the ceiling ofx / k
)
The formula (x + k - 1) // k
elegantly computes the ceiling division without using floating-point arithmetic. This is equivalent to math.ceil(x / k)
but avoids potential precision issues.
By using binary search on the range [1, max(piles)]
and checking if each candidate speed allows Koko to finish within h
hours, we can efficiently find the minimum valid speed in O(n * log(max(piles)))
time, where n
is the number of piles.
Learn more about Binary Search patterns.
Solution Approach
The solution implements binary search to find the minimum eating speed. Here's how the algorithm works:
1. Set up the binary search boundaries:
- Left boundary
l = 1
(minimum possible eating speed) - Right boundary
r = max(piles)
(maximum useful eating speed)
2. Define the check function:
The helper function check(k)
determines if Koko can finish all bananas at speed k
within h
hours:
def check(k: int) -> bool:
return sum((x + k - 1) // k for x in piles) <= h
- For each pile
x
, calculate hours needed:(x + k - 1) // k
(ceiling division) - Sum all hours needed across all piles
- Return
True
if total hours ≤h
, otherwiseFalse
3. Apply binary search:
The code uses Python's bisect_left
function in an elegant way:
return 1 + bisect_left(range(1, max(piles) + 1), True, key=check)
This works because:
bisect_left
searches for the leftmost position whereTrue
can be inserted in a sorted sequence- The
key=check
parameter transforms each speed value into a boolean (can Koko finish in time?) - For speeds too slow,
check
returnsFalse
; for speeds fast enough, it returnsTrue
- Since we want speeds that work (
True
values), andFalse < True
in Python, the sequence looks like:[False, False, ..., False, True, True, ..., True]
bisect_left
finds the firstTrue
position, which corresponds to the minimum valid speed- Adding 1 adjusts for the 0-based indexing of
bisect_left
on the range starting from 1
Time Complexity: O(n * log(max(piles)))
where n
is the number of piles
- Binary search runs in
O(log(max(piles)))
iterations - Each iteration calls
check
, which takesO(n)
time to sum over all piles
Space Complexity: O(1)
- only using constant extra space for variables
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example: piles = [3, 6, 7, 11]
, h = 8
Step 1: Initialize binary search boundaries
- Left boundary:
l = 1
(minimum eating speed) - Right boundary:
r = max([3, 6, 7, 11]) = 11
(maximum useful speed) - Search range:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Step 2: Apply binary search with the check function
The check(k)
function calculates if speed k
allows finishing within 8 hours:
- For each pile, we calculate hours needed using
(pile + k - 1) // k
Let's evaluate check
for different speeds:
k = 1
: Hours needed =(3+0)//1 + (6+0)//1 + (7+0)//1 + (11+0)//1 = 3 + 6 + 7 + 11 = 27
→27 > 8
→False
k = 2
: Hours needed =(3+1)//2 + (6+1)//2 + (7+1)//2 + (11+1)//2 = 2 + 4 + 4 + 6 = 16
→16 > 8
→False
k = 3
: Hours needed =(3+2)//3 + (6+2)//3 + (7+2)//3 + (11+2)//3 = 1 + 3 + 3 + 5 = 12
→12 > 8
→False
k = 4
: Hours needed =(3+3)//4 + (6+3)//4 + (7+3)//4 + (11+3)//4 = 1 + 2 + 2 + 3 = 8
→8 ≤ 8
→True
k = 5
: Hours needed =(3+4)//5 + (6+4)//5 + (7+4)//5 + (11+4)//5 = 1 + 2 + 2 + 3 = 8
→8 ≤ 8
→True
The sequence of check
results looks like:
Speed: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] Result: [False, False, False, True, True, True, True, True, True, True, True]
Step 3: Find the minimum valid speed
Using bisect_left
:
- We search for the leftmost position where
True
can be inserted - The first
True
appears at index 3 (corresponding to speed 4) - Since we're searching in
range(1, 12)
, we add 1 to convert from 0-based index to actual speed - Result:
1 + 3 = 4
Verification for k = 4:
- Pile 1 (3 bananas): needs
⌈3/4⌉ = 1
hour - Pile 2 (6 bananas): needs
⌈6/4⌉ = 2
hours - Pile 3 (7 bananas): needs
⌈7/4⌉ = 2
hours - Pile 4 (11 bananas): needs
⌈11/4⌉ = 3
hours - Total:
1 + 2 + 2 + 3 = 8
hours ✓
The minimum eating speed is 4 bananas per hour.
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4class Solution:
5 def minEatingSpeed(self, piles: List[int], h: int) -> int:
6 """
7 Find the minimum eating speed to consume all banana piles within h hours.
8
9 Args:
10 piles: List of integers representing the number of bananas in each pile
11 h: Maximum hours available to eat all bananas
12
13 Returns:
14 Minimum integer eating speed (bananas per hour) to finish all piles
15 """
16
17 def can_finish_in_time(eating_speed: int) -> bool:
18 """
19 Check if all piles can be consumed within h hours at given eating speed.
20
21 Args:
22 eating_speed: Number of bananas that can be eaten per hour
23
24 Returns:
25 True if all piles can be finished within h hours, False otherwise
26 """
27 # Calculate total hours needed to eat all piles at current speed
28 # Using ceiling division: (x + k - 1) // k is equivalent to math.ceil(x / k)
29 total_hours = sum((pile_size + eating_speed - 1) // eating_speed
30 for pile_size in piles)
31 return total_hours <= h
32
33 # Binary search for the minimum valid eating speed
34 # Search range: from 1 to max(piles) bananas per hour
35 # bisect_left finds the leftmost position where can_finish_in_time returns True
36 min_speed = 1 + bisect_left(range(1, max(piles) + 1), True,
37 key=can_finish_in_time)
38
39 return min_speed
40
1class Solution {
2 public int minEatingSpeed(int[] piles, int h) {
3 // Binary search bounds: minimum speed is 1, maximum is 10^9
4 int left = 1;
5 int right = (int) 1e9;
6
7 // Binary search to find the minimum eating speed
8 while (left < right) {
9 // Calculate middle speed
10 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
11
12 // Calculate total hours needed at current speed
13 int totalHours = 0;
14 for (int pile : piles) {
15 // Calculate hours to eat current pile (ceiling division)
16 // (pile + mid - 1) / mid is equivalent to Math.ceil(pile / mid)
17 totalHours += (pile + mid - 1) / mid;
18 }
19
20 // Check if current speed is valid
21 if (totalHours <= h) {
22 // Current speed works, try to find a smaller speed
23 right = mid;
24 } else {
25 // Current speed is too slow, need to increase speed
26 left = mid + 1;
27 }
28 }
29
30 // Return the minimum valid eating speed
31 return left;
32 }
33}
34
1class Solution {
2public:
3 int minEatingSpeed(vector<int>& piles, int h) {
4 // Binary search range: minimum speed is 1, maximum is the largest pile
5 int left = 1;
6 int right = ranges::max(piles);
7
8 // Binary search to find the minimum eating speed
9 while (left < right) {
10 // Calculate middle speed (using bit shift for division by 2)
11 int mid = (left + right) >> 1;
12
13 // Calculate total hours needed at current speed
14 int totalHours = 0;
15 for (int pile : piles) {
16 // Ceiling division: (pile + mid - 1) / mid equals ceil(pile / mid)
17 // This calculates hours needed to eat current pile at speed 'mid'
18 totalHours += (pile + mid - 1) / mid;
19 }
20
21 // If we can finish within h hours, try slower speed (smaller k)
22 if (totalHours <= h) {
23 right = mid; // mid could be the answer, keep it in range
24 } else {
25 // If we can't finish in time, need faster speed
26 left = mid + 1; // mid is too slow, exclude it
27 }
28 }
29
30 // left == right, this is the minimum eating speed
31 return left;
32 }
33};
34
1/**
2 * Finds the minimum eating speed to consume all piles within h hours
3 * Uses binary search to find the optimal speed
4 * @param piles - Array of pile sizes
5 * @param h - Maximum hours allowed to eat all piles
6 * @returns Minimum eating speed (bananas per hour)
7 */
8function minEatingSpeed(piles: number[], h: number): number {
9 // Initialize binary search boundaries
10 // Minimum speed is 1, maximum speed is the largest pile size
11 let left: number = 1;
12 let right: number = Math.max(...piles);
13
14 // Binary search for the minimum valid speed
15 while (left < right) {
16 // Calculate middle point using bit shift for integer division
17 const middle: number = (left + right) >> 1;
18
19 // Calculate total hours needed at current speed
20 // For each pile, calculate hours needed (rounded up)
21 const totalHours: number = piles
22 .map((pileSize: number) => Math.ceil(pileSize / middle))
23 .reduce((accumulator: number, current: number) => accumulator + current, 0);
24
25 // Check if current speed is valid
26 if (totalHours <= h) {
27 // Current speed works, try to find a smaller valid speed
28 right = middle;
29 } else {
30 // Current speed is too slow, need to increase speed
31 left = middle + 1;
32 }
33 }
34
35 // Return the minimum valid eating speed
36 return left;
37}
38
Time and Space Complexity
The time complexity is O(n × log M)
, where n
is the length of the array piles
and M
is the maximum value in piles
. This is because:
- The binary search using
bisect_left
searches through the range[1, max(piles)]
, which hasM
elements, takingO(log M)
iterations - For each iteration of the binary search, the
check
function is called, which computes the sum of(x + k - 1) // k
for all elements inpiles
, takingO(n)
time - Therefore, the total time complexity is
O(n × log M)
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for:
- The variable
k
in thecheck
function - The internal variables used by
bisect_left
- The generator expression in the
sum
function doesn't create a list but computes values on-the-fly
Note that while range(1, max(piles) + 1)
is passed to bisect_left
, the implementation of bisect_left
with a key
function doesn't materialize the entire range into memory but treats it as virtual bounds for the binary search.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Search Boundaries
A common mistake is setting the right boundary as sum(piles)
or sum(piles) // h
instead of max(piles)
. While these might seem logical, they're inefficient and can cause issues:
Why it's wrong:
- If
k > max(piles)
, Koko still takes 1 hour per pile (she can't eat faster than the pile size) - Setting
r = sum(piles)
creates an unnecessarily large search space - Using
sum(piles) // h
might be too small if piles are unevenly distributed
Correct approach:
# Right boundary should be max(piles)
left, right = 1, max(piles)
2. Incorrect Ceiling Division
Many developers incorrectly calculate the hours needed for each pile:
Common mistakes:
# Wrong: Simple division truncates, underestimating hours needed
hours = pile // k # This would give 2 for pile=7, k=3 (should be 3)
# Wrong: Using math.ceil without importing
hours = ceil(pile / k) # NameError if math not imported
# Wrong: Float division issues
hours = int(pile / k) + (1 if pile % k > 0 else 0) # Verbose and error-prone
Correct approach:
# Efficient ceiling division without imports hours = (pile + k - 1) // k # Or alternatively: hours = -(-pile // k) # Using negative division trick
3. Off-by-One Error in Binary Search
When implementing manual binary search, it's easy to get the boundaries wrong:
Common mistake:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
left, right = 1, max(piles)
while left < right: # Wrong: might miss the answer
mid = (left + right) // 2
if can_finish(mid):
right = mid - 1 # Wrong: might skip the minimum valid speed
else:
left = mid + 1
return left # Might return invalid speed
Correct approach:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
left, right = 1, max(piles)
while left < right:
mid = (left + right) // 2
if can_finish(mid):
right = mid # Keep mid as potential answer
else:
left = mid + 1
return left
4. Integer Overflow in Other Languages
While not an issue in Python, developers coming from other languages might worry about overflow:
Potential issue in languages like Java/C++:
# This could overflow in other languages mid = (left + right) // 2
Safe approach (habit from other languages):
mid = left + (right - left) // 2
5. Misunderstanding the bisect_left Usage
The clever use of bisect_left
with a boolean key can be confusing:
Common confusion:
# Wrong: Forgetting to add 1 to the result
return bisect_left(range(1, max(piles) + 1), True, key=check) # Off by 1
# Wrong: Using bisect_right instead
return bisect_right(range(1, max(piles) + 1), True, key=check) # Wrong boundary
Why the +1 is needed:
bisect_left
returns the index in the range whereTrue
would be inserted- Since our range starts at 1 (not 0), we need to add 1 to get the actual speed value
- The range
[1, 2, 3, ..., max(piles)]
has indices[0, 1, 2, ..., max(piles)-1]
How many times is a tree node visited in a depth first search?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!