Problem overview
Koko has several piles of bananas and h hours before the guards return. She picks a speed of k bananas per hour. Each hour she chooses one pile and eats up to k bananas from it; if the pile has fewer, she finishes it and wastes the rest of that hour. So a pile of p bananas takes p / k hours, rounded up.
The function minEatingSpeed(piles: IntArray, h: Int): Int returns the smallest speed that finishes every pile within h hours.
With piles = [3, 6, 7, 11] and h = 8, speed 4 takes 1 + 2 + 2 + 3 = 8 hours, just in time, while speed 3 takes 1 + 2 + 3 + 4 = 10 hours. The answer is 4.
The lesson asks for O(n log m) time, where m is the biggest pile, and O(1) extra space.
Approach: binary search on the speed
The simple method tries speeds 1, 2, 3, and so on, and stops at the first one that works. Each try costs O(n), and the answer can be as large as the biggest pile, so that is O(n · m): too slow when piles reach a billion bananas.
The speeds have a useful shape. If Koko finishes in time at speed k, she finishes at any faster speed as well. If she doesn't, no slower speed works either. So going from speed 1 upward, the speeds are first too slow and then fast enough, and the switch happens only once. Whenever answers flip from "no" to "yes" exactly once, binary search can find the flip point. Here the search runs over the range of possible speeds rather than over an array.
The range is easy to bound. Speed 1 is the slowest sensible speed. The biggest pile is the fastest speed that's ever useful: at that speed every pile takes one hour, and since there are never more piles than hours, it always works. So the answer lies between 1 and piles.max(), and the search needs about log2(m) rounds, each costing one pass over the piles.
Step by step
- Set the range.
leftstarts at 1 andrightatpiles.max(). Both arevars, since the search moves them. - Remember a working speed.
answerstarts atright, which is known to work, so it is valid even before the first test. - Loop while
left <= right. When they are equal, one speed is still untested. - Pick a middle speed
kasleft + (right - left) / 2. - Test it with a helper,
hours(piles, k), which returns the total hours at that speed. If it is at mosth, the speed works. - If it works, save
kinanswerand search for something slower by movingrighttok - 1. Save first: the search never returns tok, and it may be the best speed. - If it doesn't, every slower speed fails too, so move
lefttok + 1. - Return
answeronce the range is empty. Every speed below it was ruled out. - Write the helper
hours. It takes the piles and a speed and returns aLong. - Start a
Longtotal with0L. - Loop over the piles. Koko never eats from two piles in one hour, so each pile adds its own hours.
- Round up with integers only:
(pile + k - 1) / k. - Return the total.
The solution in Kotlin
class Solution {
fun minEatingSpeed(piles: IntArray, h: Int): Int {
var left = 1
var right = piles.max()
var answer = right
while (left <= right) {
val k = left + (right - left) / 2
if (hours(piles, k) <= h) {
answer = k
right = k - 1
} else {
left = k + 1
}
}
return answer
}
fun hours(piles: IntArray, k: Int): Long {
var total = 0L
for (pile in piles) {
total += (pile + k - 1) / k
}
return total
}
}
Kotlin details worth a look:
- Rounding up. Integer division truncates, so
7 / 3is 2, which forgets the last hour. Addingk - 1first pushes any leftover past the next multiple:(7 + 3 - 1) / 3is 3. An exact multiple doesn't move, so(6 + 3 - 1) / 3is still 2. The sumpile + k - 1stays below 2,000,000,000 for piles and speeds up to a billion, so it fits in anInt. - Why the helper returns a
Long. With 10,000 piles of 1,000,000,000 bananas, speed 1 needs 10,000,000,000,000 hours. AnInttops out near 2.1 billion; summing those piles in anIntgives1316134912instead of 10,000,000,000,000. Declaringtotalwith0Lmakes it aLong. - Adding an
Intto aLong.total += ...with anInton the right compiles, becauseLonghas aplusoverload forIntand the result is aLong. - Comparing
LongwithInt.hours(piles, k) <= hcompiles directly, sinceLonghas acompareTooverload forInt. Mixed==between the two types is not allowed, but<=is. piles.max(). On anIntArrayit returns the largest element as a plainInt. It throws on an empty array, which the constraints rule out.- Two functions in one class.
hoursis a member ofSolution, sominEatingSpeedcalls it by name, and it may appear below its first use.
Complexity
Time: O(n log m). The search range holds m speeds and halves every round, so there are about log2(m) rounds, around 30 for a billion. Each round calls hours, which makes one pass over the n piles.
Space: O(1). Only a few numbers are kept.
Edge cases and common mistakes
- One pile.
[5]withh = 1returns 5, and[1000000000]withh = 2returns 500000000. - As many hours as piles. Every pile must take exactly one hour, so the answer is the biggest pile:
[30, 11, 23, 4, 20]withh = 5returns 30. - Rounding down instead of up. Plain
pile / kundercounts every pile that doesn't divide evenly, so the function would accept speeds that are too slow. - Overflow in the total. Summing hours in an
Intwraps silently to a wrong, possibly negative, number, and the comparison withhthen gives a wrong yes. Use aLong. - Losing the answer. If
answerisn't saved beforerightmoves, the loop can end without remembering the speed that worked. - Using
left < right. The final candidate speed would never be tested.
Testing
Trace for piles = [3, 6, 7, 11] and h = 8:
left=1 right=11 k=6 hours=6
left=1 right=5 k=3 hours=10
left=4 right=5 k=4 hours=8
Speed 6 works (6 hours), so the range moves down. Speed 3 needs 10 hours, so the range moves up. Speed 4 needs exactly 8 hours and works; the range then becomes empty, and the answer is 4.
piles = [3, 6, 7, 11],h = 8gives4.piles = [30, 11, 23, 4, 20],h = 5gives30.piles = [30, 11, 23, 4, 20],h = 6gives23.- 10,000 piles of 1,000,000,000 with
h = 10000gives1000000000.
