Problem overview
The method MinEatingSpeed receives an int[] of banana piles and an int h, the number of hours available. Koko picks a speed k in bananas per hour. Each hour she works on one pile and eats up to k bananas from it. If the pile has fewer than k, she finishes it and wastes the rest of that hour. A pile of p bananas therefore takes p / k hours, rounded up. The method returns the smallest whole speed that finishes every pile 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, while at speed 3 they take 1 + 2 + 3 + 4 = 10. For {30, 11, 23, 4, 20} with h = 5 there are as many hours as piles, so every pile must take one hour and the answer is the biggest pile, 30. With h = 6 the answer is 23.
Approach: binary search on the answer, not on the data
Nothing in the input is sorted, so what is there to binary search? The speeds. 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 going through the speeds 1, 2, 3, and so on, the answer is "too slow" up to some point and "fast enough" from then on, with a single switch. Binary search finds that switch.
The search range is easy to bound. The slowest sensible speed is 1. The fastest we ever need is the biggest pile: 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().
Trying every speed from 1 up costs O(n · m), where m is the biggest pile. Binary search needs only about log2(m) speeds (about 30 for a billion bananas), and each test of a speed reads the piles once, so the total is O(n log m).
On {3, 6, 7, 11} with h = 8, the search tries speed 6 (6 hours, fast enough, so look lower), then 3 (10 hours, too slow, so look higher), then 4 (8 hours, fast enough). The range is then empty and the answer is 4.
Step by step
- Set the range.
left = 1andright = piles.Max().Max()comes fromSystem.Linqand returns the largest element. - Remember a speed that works.
answerstarts atright, which is known to be fast enough, so there is a valid result even before the first test. - 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.
Hours(piles, k), written later, returns the hours needed at speedk. If that is at mosth, the speed works. - If it works, store
kinanswerfirst, then look for something slower withright = k - 1. Store before moving: the search never returns tok, and it may be the best speed there is. - Otherwise it is too slow, and so is every slower speed, so
left = k + 1. - Return
answer. When the loop ends, every speed belowanswerwas ruled out. - Write
Hours. It returns along, startstotalat 0 and adds, for each pile, the pile's hours rounded up. Integer-only rounding up is(pile + k - 1) / k: a pile of 7 at speed 3 takes (7 + 2) / 3 = 3 hours, while7 / 3alone gives 2 and forgets the last hour. An exact multiple doesn't move: (6 + 2) / 3 is still 2.
The solution in C#
class Solution {
public int MinEatingSpeed(int[] piles, int h) {
int left = 1, right = piles.Max();
int answer = right;
while (left <= right) {
int k = left + (right - left) / 2;
if (Hours(piles, k) <= h) {
answer = k;
right = k - 1;
} else {
left = k + 1;
}
}
return answer;
}
public long Hours(int[] piles, int k) {
long total = 0;
foreach (int pile in piles) {
total += (pile + k - 1) / k;
}
return total;
}
}
The C# details worth knowing:
- Why
Hoursreturns along. With 10,000 piles of 1,000,000,000 bananas, speed 1 takes 10,000,000,000,000 hours. Aninttops out at 2,147,483,647 and wraps silently: adding those same ten thousand values in anintvariable gives 1316134912, a wrong but plausible-looking number. Becausetotalis along, the comparisonHours(piles, k) <= his also done in 64 bits. total += (pile + k - 1) / kcomputes the division inint, then widens the result into thelong. That is safe here:pile + k - 1is at most 1,999,999,999 when both are 1,000,000,000, which still fits in anint.- Integer division truncates. That is why adding
k - 1before dividing rounds up.Math.Ceilingwould need floating point, which is slower and less precise for big values. - The midpoint form.
left + (right - left) / 2never overflows, unlike(left + right) / 2for large ranges. foreach (int pile in piles)reads each element in turn. The loop variable can't be assigned to, which is fine because the method only reads it.
Complexity
Let n be the number of piles and m the biggest pile.
Time: O(n log m). The search range has m speeds and each pass halves it, so about log2(m) passes. Each pass calls Hours, which makes one pass over the n piles.
Space: O(1). A few variables, with no extra collections.
Edge cases and common mistakes
- One pile.
{1000000000}withh = 1returns 1000000000, and withh = 312884469for a pile of 312884470 it returns 2, since speed 1 would need 312884470 hours. - As many hours as piles. The answer is the biggest pile, which is exactly the upper end of the range.
- Rounding down. Using
pile / kgives answers that are too small, because the last partial hour is lost. - An
inttotal. It overflows at slow speeds and may even turn negative, which would make a slow speed look fast enough. - Forgetting
answer = k. Without it, the code has no way to know which speed was the last one that worked. - Starting
leftat 0. A speed of 0 would divide by zero inHours, which throwsDivideByZeroException.
Testing
{3, 6, 7, 11}withh = 8gives4.{30, 11, 23, 4, 20}withh = 5gives30, and withh = 6gives23.{1000000000}withh = 1gives1000000000.{1000000000, 1000000000}withh = 2gives1000000000.
