Problem overview
minEatingSpeed receives piles, an array of banana counts, and h, the number of hours Koko has. She picks a speed k bananas per hour. Each hour she works on one pile and eats up to k bananas from it; if the pile has fewer, she finishes it and wastes the rest of the hour. A pile of p bananas therefore takes p / k hours, rounded up. The function returns the smallest integer k that gets through all the piles within h hours.
For piles = [3, 6, 7, 11] and h = 8 the answer is 4. At speed 4 the piles take 1 + 2 + 2 + 3 = 8 hours, just in time. At speed 3 they take 1 + 2 + 3 + 4 = 10 hours, which is too many.
The task asks for O(n log m) time, where n is the number of piles and m the biggest pile, and O(1) extra space.
Approach: binary search on the speed
The simple idea is to try speed 1, then 2, then 3, and stop at the first speed that works. That can take up to m attempts, and each attempt costs O(n), which is far too slow when a pile holds a billion bananas.
The better idea uses one fact: if Koko finishes in time at speed k, she also finishes at any faster speed, and if she doesn't, no slower speed works either. So the speeds split into a block of "too slow" followed by a block of "fast enough", and the answer is the first speed of the second block. Binary search finds that boundary. The search runs over speeds, not over positions in the array, which is why it is called binary search on the answer.
The search range is easy to set. The slowest sensible speed is 1. The fastest worth trying is the biggest pile: at that speed every pile takes one hour, and since there are never more piles than hours, it always works. No answer is ever larger. The range 1 to piles.max()! has about log2(m) halvings, around 30 for a billion.
Each halving needs the total hours at one speed, which is a plain loop over the piles. That part lives in a helper method named hours.
Step by step
- Set the range. Declare
leftas 1 andrightas the biggest pile, both withvarbecause the search moves them. - Remember a speed that works.
answerstarts atright, which is known to be fast enough, so it holds a valid speed even before the search begins. - Loop while
left <= right. When they are equal, one speed is still untried. - Take the middle speed
k = left + (right - left) / 2. - Ask the helper. If
hours(piles, k) <= h, speedkfinishes in time. - Speed
kworks: store it inanswer, then look for a slower one by movingrighttok - 1. Store first: the search never returns tok, and it may be the smallest working speed. - Speed
kis too slow: in theelsebranch, movelefttok + 1. Every slower speed fails too. - After the loop every speed below
answerhas been ruled out, so return it. - Write the helper
hoursinside the class. It takes the piles and a speed, both without argument labels, and returns anInt. - Keep a running total in a
varstarting at 0. - Loop over the piles with
for pile in piles. Koko never eats from two piles in the same hour, so each pile adds its own hours. - Round up with integers only. Add
(pile + k - 1) / k. Addingk - 1pushes any leftover bananas over the next multiple ofk, while an exact multiple stays where it is. - Return the total.
The solution in Swift
class Solution {
func minEatingSpeed(_ piles: [Int], _ h: Int) -> Int {
var left = 1, right = piles.max()!
var answer = right
while left <= right {
let k = left + (right - left) / 2
if hours(piles, k) <= h {
answer = k
right = k - 1
} else {
left = k + 1
}
}
return answer
}
func hours(_ piles: [Int], _ k: Int) -> Int {
var total = 0
for pile in piles {
total += (pile + k - 1) / k
}
return total
}
}
Swift details worth knowing:
max()returns an optional. On an empty array[Int]().max()isnil. The lesson guarantees at least one pile, so the!unwrap is safe here. On an empty array it would stop the program.- Integer rounding.
/on twoIntvalues truncates. For a pile of 7 at speed 3,7 / 3is 2 and forgets the last hour, while(7 + 2) / 3is 3. For a pile of 6,(6 + 2) / 3is still 2. - Big totals fit in
Int.Intis 64 bits. With 10,000 piles of a billion bananas, speed 1 needs 10,000,000,000,000 hours, and the total fits. Swift stops the program on integer overflow instead of wrapping, so anInt32total would be a real danger here, andIntavoids it. - Calling a method of the same class. Inside
minEatingSpeed,hours(piles, k)is a call onself, so no prefix is needed. The method can be defined below the code that calls it. - Multiple
vars on one line.var left = 1, right = piles.max()!declares two variables, bothInt.
Complexity
Time: O(n log m). The range 1 to m halves on every pass, so the loop runs about log2(m) times. Each pass calls hours, which visits all n piles once. Finding the maximum at the start costs another O(n).
Space: O(1). The code keeps a few integers. The helper doesn't build any array.
Edge cases and common mistakes
- Rounding down instead of up. Using
pile / kundercounts every pile that doesn't divide evenly, so the function returns speeds that are too slow. - Exactly as many hours as piles. With
piles = [30, 11, 23, 4, 20]andh = 5, every pile must take one hour, so the answer is the biggest pile, 30. This is whyrightstarts at the maximum andanswerstarts there too. - One pile.
[5]withh = 1gives 5, and withh = 5gives 1. A single pile of a billion withh = 2gives 500,000,000. - Moving
rightbefore savingk. If the order of the two lines in the success branch is swapped, the value can be lost. - Returning
leftwithout trackinganswer. That form also works if you are careful, but it is easy to be off by one. Saving each working speed is simpler to get right. left = korright = k. Without the+ 1and- 1, the range can stop shrinking and the loop never ends.
Testing
piles = [3, 6, 7, 11],h = 8gives 4. The helper returns 10 for speed 3 and 8 for speed 4.piles = [30, 11, 23, 4, 20],h = 5gives 30.piles = [30, 11, 23, 4, 20],h = 6gives 23. The helper returns 7 for speed 22 and 6 for speed 23.piles = [5],h = 1gives 5, andh = 5gives 1.piles = [1, 1, 1],h = 3gives 1.
On the first example the search tries 6 (6 hours, works, so right becomes 5), then 3 (10 hours, too slow, so left becomes 4), then 4 (8 hours, works, so right becomes 3) and stops with answer = 4.
