Facebook Pixel

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 exactly k 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 by k, she needs exactly x / k hours
  • If not, she needs x // k + 1 hours (the ceiling of x / 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, otherwise False

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 where True 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 returns False; for speeds fast enough, it returns True
  • Since we want speeds that work (True values), and False < True in Python, the sequence looks like: [False, False, ..., False, True, True, ..., True]
  • bisect_left finds the first True 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 takes O(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 Evaluator

Example 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 = 2727 > 8False
  • k = 2: Hours needed = (3+1)//2 + (6+1)//2 + (7+1)//2 + (11+1)//2 = 2 + 4 + 4 + 6 = 1616 > 8False
  • k = 3: Hours needed = (3+2)//3 + (6+2)//3 + (7+2)//3 + (11+2)//3 = 1 + 3 + 3 + 5 = 1212 > 8False
  • k = 4: Hours needed = (3+3)//4 + (6+3)//4 + (7+3)//4 + (11+3)//4 = 1 + 2 + 2 + 3 = 88 ≤ 8True
  • k = 5: Hours needed = (3+4)//5 + (6+4)//5 + (7+4)//5 + (11+4)//5 = 1 + 2 + 2 + 3 = 88 ≤ 8True

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 has M elements, taking O(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 in piles, taking O(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 the check 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 where True 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]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many times is a tree node visited in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More