Problem overview
The method minEatingSpeed receives an array piles of banana counts and an integer h, the hours Koko has. She picks a speed k, bananas per hour. Each hour she chooses one pile and eats up to k bananas from it; if the pile is smaller, she finishes it and wastes the rest of the hour. A pile of p bananas therefore takes p / k hours, rounded up. The method returns the smallest k that finishes all piles within h hours.
For piles = [3, 6, 7, 11] and h = 8, speed 4 takes 1 + 2 + 2 + 3 = 8 hours, just in time. Speed 3 takes 1 + 2 + 3 + 4 = 10 hours, too many. The answer is 4.
Approach: binary search on the answer
The speed has a limited range. The slowest useful speed is 1. The fastest we ever need is the biggest pile, piles.max: at that speed every pile takes one hour, and since h is at least the number of piles, it always fits. So the answer lies between 1 and piles.max.
Trying every speed in that range would take O(n · m) time, where m is the biggest pile. The key observation is that the speeds are ordered: if Koko finishes at speed k, she finishes at any faster speed too, and if she doesn't, no slower speed works either. The speeds turn from "too slow" to "fast enough" exactly once, and binary search finds where. We search over answers instead of over array positions.
Each test of a speed needs a helper, hours(piles, k), which adds up the hours of all the piles in O(n). Binary search tests about log2(m) speeds, so the total is O(n log m): around 30 tests for piles of up to a billion bananas.
Step by step
- Set the range.
leftis 1 andrightispiles.max, bothvars. Both ends are inclusive candidates. - Start
answeratright. The biggest pile always works, soanswerholds a valid speed before the search begins. It will hold the smallest working speed found so far. - Loop while
left <= right. When they are equal, one speed is still untried. - Pick the middle speed
k = left + (right - left) / 2. Integer division rounds down, and this form can't overflow. - Test the speed with
hours(piles, k) <= h. - If it works, save
kinanswerfirst, then look for something slower withright = k - 1. The search never returns tok, and it may be the best speed there is. - If it is too slow, so is every slower speed:
left = k + 1. - Return
answerafter the loop. Every speed below it was ruled out as too slow. - Write the helper
hours, taking the piles and a speedkand returning aLong. - Start
totalat0L, avar. TheLmakes it aLong. - Loop over the piles, since Koko never eats from two piles in one hour and each pile adds its own hours.
- Add the pile's hours, rounded up, using only integers:
(pile + k - 1) / k. - Return
total.
The solution in Scala
class Solution {
def minEatingSpeed(piles: Array[Int], 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
}
}
answer
}
def hours(piles: Array[Int], k: Int): Long = {
var total = 0L
for (pile <- piles) {
total += (pile + k - 1) / k
}
total
}
}
The Scala details worth a closer look:
- Rounding up with integers. Integer division rounds down, so
7 / 3is 2 and forgets the last hour. Addingk - 1first pushes any leftover past the next multiple ofk:(7 + 2) / 3is 3. An exact multiple doesn't move:(6 + 2) / 3is still 2. NoMath.ceilorDoubleis needed. Longfor the total. At speed 1, 10,000 piles of a billion bananas take 10,000,000,000,000 hours, far beyond what anIntholds (about 2.1 billion).totalstarts as0L, andtotal += (pile + k - 1) / kadds anIntto aLong, which stays aLong. With a plainIntcounter, that same sum wraps silently to 1316134912, a number unrelated to the real count.- Comparing
LongwithInt.hours(piles, k) <= hcompares aLongwith anInt; Scala widens theInt, so no conversion is needed. - The last expression is the value.
answerandtotalstand alone on the last line of their methods, which is how Scala returns them. piles.maxreturns the largest element and throws an exception on an empty array. The constraints guarantee at least one pile.- The helper is a method of the class, so
minEatingSpeedcalls it by name.
Complexity
Time: O(n log m). The search range has m candidate speeds and halves on every pass, so there are about log2(m) passes. Each pass calls hours, which visits all n piles once.
Space: O(1). Only a few integers and the running Long total are kept.
Edge cases and common mistakes
- One pile. With
piles = [1000000000]andh = 1the answer is 1000000000. With the same pile andh = 1000000000it is 1. Both work without special cases. - As many hours as piles. For
[30, 11, 23, 4, 20]andh = 5, every pile must take exactly one hour, so the answer is the biggest pile, 30. This is whyrightstarts atpiles.max. - A pile that takes two hours at the answer. For the same piles and
h = 6, speed 23 takes 2 + 1 + 1 + 1 + 1 = 6 hours. At 22 the pile of 23 still takes 2 hours, and the total is 7, so the answer is 23. - Rounding down. Using
pile / kalone undercounts every pile that isn't an exact multiple, and the search returns a speed that is too slow. - Overflow in the hours. A 32-bit total gives a wrong answer for large inputs: the hours at speed 1 for ten thousand piles of a billion bananas wrap around to 1316134912, which has nothing to do with the real count, and the comparison with
hbecomes meaningless. Keeping the total aLongavoids it. - Overflow in
pile + k - 1. This sum is anInt. It stays safe because bothpileandkare at most a billion, so the sum is below 2 billion. - Forgetting to save
answer. Ifright = k - 1runs without storingk, the best speed can be lost, because the search moves past it.
Testing
piles = [3, 6, 7, 11],h = 8gives 4. The hours at speeds 1, 2, 3 and 4 are 27, 15, 10 and 8.piles = [30, 11, 23, 4, 20],h = 5gives 30.piles = [30, 11, 23, 4, 20],h = 6gives 23.piles = [1],h = 1gives 1.- 10,000 piles of 1000000000 bananas with
h = 1000000000gives 10000.
