Problem overview
Koko has several piles of bananas, piles[i] in pile i, and h hours before the guards return. At a speed of k bananas per hour she eats from one pile each hour. If the pile has fewer than k bananas she finishes it and wastes the rest of that hour. So a pile of p bananas takes p / k hours, rounded up.
The method minEatingSpeed returns the smallest integer k that lets her eat everything 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. For piles = [30, 11, 23, 4, 20] and h = 5 there are as many hours as piles, so each pile must go in one hour and the speed is the biggest pile, 30. With h = 6 the answer is 23.
Approach: binary search on the answer
The unusual thing here is what gets searched. There is no sorted list to look into. Instead, the candidate speeds 1, 2, 3, ... form an ordered range, and a single question sorts them: does speed k finish within h hours?
That question has a useful shape. If Koko finishes at speed k, she finishes at any faster speed too. If she doesn't, no slower speed works either. So going from speed 1 upward, the answer to the question is "no" for a while and then "yes" forever. For the first example:
speed 1 2 3 4 5 11
hours 27 15 10 8 8 4
With h = 8, speeds 1 to 3 are too slow, and speeds 4 and up are fast enough. The answer is the first "yes", and binary search can find where the answer flips to "yes" without trying every speed.
The range to search is small. The slowest sensible speed is 1. The fastest that could ever be needed is the biggest pile, because at that speed every pile takes one hour, and the constraints say h is at least the number of piles. No answer is bigger than max(piles), so the search runs over 1 to max(piles). That is why the time is O(n log m), with m the biggest pile: about log m speeds are tried, and each try reads all n piles.
Here is the search on the first example:
left=1 right=11 k=6 hours=6 ok=True
left=1 right=5 k=3 hours=10 ok=False
left=4 right=5 k=4 hours=8 ok=True
Speed 6 works, so it is stored as a candidate and the search looks at slower speeds. Speed 3 is too slow, so the search looks above it. Speed 4 works. That leaves nothing smaller to try, and the stored answer is 4.
Step by step
- Set the range.
leftis 1 andrightismax(piles), both set in one tuple assignment. - Start
answeratright. That speed always works, soanswerholds a valid speed before the search even begins. - Loop while
left <= right. When both ends are the same speed, that speed still needs a try. - Pick
khalfway between the ends with floor division. - Ask the question by calling a helper,
self.hours(piles, k), and comparing the result withh. The helper is written afterwards; the search only needs to know it exists. - If the speed works, save it in
answerand moverighttok - 1to look for something slower. Save first: the search never returns tok, and it may be the best speed there is. - Otherwise,
kand every slower speed are too slow, soleftbecomesk + 1. - After the loop, return
answer. Every speed below it was ruled out. - Write the helper
hours(self, piles, k)inside the class, afterminEatingSpeed. - Start
totalat 0 and loop over the piles. Koko never eats from two piles in the same hour, so each pile adds its own hours. - Round up with integers. Add
pile + k - 1and floor-divide byk. - Return
total.
The solution in Python
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
left, right = 1, max(piles)
answer = right
while left <= right:
k = left + (right - left) // 2
if self.hours(piles, k) <= h:
answer = k
right = k - 1
else:
left = k + 1
return answer
def hours(self, piles, k):
total = 0
for pile in piles:
total += (pile + k - 1) // k
return total
Python details worth a closer look:
- Rounding up without floats.
//rounds down, so7 // 3is 2 and forgets the last hour. Addingk - 1first pushes any leftover past the next multiple ofk:(7 + 2) // 3is 3. A pile that is an exact multiple doesn't move:(6 + 2) // 3is still 2. The expressionmath.ceil(pile / k)gives the same numbers but goes through a float, so the integer form is the safer habit. - Big numbers are fine. Python integers grow as needed. With 10,000 piles of 1,000,000,000 bananas, speed 1 takes 10000000000000 hours, which a 32-bit integer couldn't hold, and
hoursreturns it without trouble. - Calling a method of the class.
hoursis a method, so the search calls it asself.hours(...). Writinghours(piles, k)alone would raise aNameError. - Order of definitions.
minEatingSpeeduseshoursbefore the code ofhoursappears in the file. That is fine: the name is looked up when the method runs, by which time the class is complete. - Floor division of the midpoint.
left + (right - left) // 2keepskan integer. With/it would be a float, and the rounding inhourswould be wrong.
Complexity
Time: O(n log m). The range holds m = max(piles) speeds, and each pass of the loop discards half of them, so there are about log2(m) passes: roughly 30 when m is 1,000,000,000. Each pass calls hours, which makes one pass over the n piles.
Space: O(1). The search keeps a few integers, and hours only adds up a total.
Edge cases and common mistakes
- One pile. For
[10]withh = 10the answer is 1, because one banana per hour for 10 hours is enough. Withh = 1it is 10, the whole pile in one hour. - Exactly as many hours as piles. The answer is
max(piles), as in the second example. In that case the search never finds anything better than its startinganswer. - Plenty of time. For
[1, 1, 1, 1]the answer is 1 whetherhis 4 or 100. The speed can't go below 1, which is whyleftstarts there. - Rounding down. Using
pile // kalone undercounts every pile that doesn't divide evenly, so the helper reports too few hours and the method returns a speed that is too slow. - Losing the answer. Moving
righttok - 1without storingkfirst throws away a speed that may be the smallest one that works. - Searching the wrong direction. If the speed works, look lower; if it doesn't, look higher. Swapping the two moves breaks the search.
- Using
left < right. The last remaining speed is never tried, so the smallest working speed can be missed when it is that last one.
Testing
piles = [3, 6, 7, 11],h = 8gives4.piles = [30, 11, 23, 4, 20],h = 5gives30.piles = [30, 11, 23, 4, 20],h = 6gives23.piles = [10],h = 10gives1.piles = [10],h = 1gives10.piles = [1000000000],h = 1gives1000000000.piles = [1, 1, 1, 1],h = 100gives1.
