Problem overview
Koko has n piles of bananas, piles[i] in pile i, and h hours before the guards return. She picks a speed of k bananas per hour. Each hour she works on a single 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.
minEatingSpeed receives the vector<int> piles and the int h, and 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. At speed 3 they take 1 + 2 + 3 + 4 = 10 hours, which is too many. For piles = {30, 11, 23, 4, 20}, h = 5 gives 30, because five hours for five piles means one pile per hour, and h = 6 gives 23.
The statement asks for O(n log m) time, where m is the biggest pile, and O(1) extra space.
Approach: binary search on the speed
Trying speeds 1, 2, 3 and so on until one fits could take as many as m attempts, each costing O(n) to count the hours. The faster idea is to search over the speeds themselves.
The speeds have a useful property. If Koko finishes in time at speed k, she also finishes at any faster speed. If she doesn't finish at k, no slower speed helps. So the speeds turn from "too slow" to "fast enough" exactly once, and binary search can find the turning point. This is binary search on the answer: instead of looking for a value in an array, the search asks a yes-or-no question about a candidate speed.
The range of candidates runs from 1 up to the biggest pile. At the speed of the biggest pile every pile takes exactly one hour, and the statement guarantees h >= n, so that speed always works. No answer is ever bigger.
Here is the search on piles = {3, 6, 7, 11} with h = 8, printing each candidate and the hours it needs:
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 needs 6 hours, which fits, so it is recorded and the search looks lower. Speed 3 needs 10, which doesn't fit, so the search moves above 3. Speed 4 needs 8, which fits, and recording it leaves the range 4..3, which is empty. The answer is 4.
Step by step
- Set the range.
leftis 1, the slowest speed, andrightis the biggest pile, found with*max_element(piles.begin(), piles.end()). The function returns an iterator, and the*reads the value it points to. - Keep an
answerthat starts atright. That speed is known to work, so the variable holds a valid answer even before the search starts. - Loop while
left <= right. When the two are equal, one speed is still untried. - Pick the middle speed
kasleft + (right - left) / 2. It rounds down, and it can't overflow the wayleft + rightcould. - Ask the question: does
hours(piles, k)come to at mosth? The helper is written afterwards. - If yes,
kworks. Store it inanswerfirst, because the search never returns to it and it may be the smallest, then look for something slower withright = k - 1. - If no,
kand every slower speed are too slow, so setleft = k + 1. - Return
answer. When the loop ends, every speed below it was too slow. - Write the helper
hours. It takespilesby reference, like the main method, and the speedk, and returns along long. - Keep a
long long totalthat starts at 0 and add each pile's hours to it. - Round up with integers only. For each pile add
(pile + k - 1) / k. A pile of 7 at speed 3 gives(7 + 2) / 3, which is 3, while7 / 3alone is 2 and forgets the last hour. An exact multiple doesn't move:(6 + 2) / 3is 2. - Return
total, the hours Koko needs at speedk.
The solution in C++
class Solution {
public:
int minEatingSpeed(vector<int>& piles, int h) {
int left = 1, right = *max_element(piles.begin(), piles.end());
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;
}
long long hours(vector<int>& piles, int k) {
long long total = 0;
for (int pile : piles) {
total += (pile + k - 1) / k;
}
return total;
}
};
The C++ details worth a closer look:
- Why
long longfor the hours. At speed 1, 10,000 piles of 1,000,000,000 bananas take 10,000,000,000,000 hours, far above the largestint(2,147,483,647). The helper returns exactly that number, and along longholds up to about 9.2 quintillion. With aninttotal, signed overflow is undefined behaviour in C++, so the slow speeds could produce wrong answers. - The comparison
hours(piles, k) <= hcompares along longwith anint. Theintis converted tolong long, so nothing is lost. - The rounding sum stays in
int.pile + k - 1is at most 1,000,000,000 + 1,000,000,000 - 1, which is below 2,147,483,647 becauseknever exceeds the biggest pile. Only the running total needs the wider type. - Calling a member function defined later. Inside a class, member functions can call each other whatever the order, so
minEatingSpeedcan usehourseven though it is written below. max_elementneeds a non-empty range, which the constraints give (n >= 1). On an empty vector, dereferencing its result would be undefined behaviour.
Complexity
Time: O(n log m). The range holds m speeds and halves with each pass, so the loop runs about log2(m) times, around 30 for a biggest pile of a billion. Each pass calls hours, which visits all n piles. Finding the maximum adds one O(n) pass.
Space: O(1). Only a few integers are kept. piles is passed by reference, so it isn't copied.
Edge cases and common mistakes
- One pile.
{5}withh = 3returns 2: speed 2 takes 3 hours, and speed 1 takes 5.{1000000000}withh = 2returns 500,000,000. - As many hours as piles.
{1, 1, 1}withh = 3returns 1, andh = nin general forces the speed of the biggest pile. - Huge input. With 10,000 piles of 1,000,000,000 and
h = 10000, the answer is 1,000,000,000. Withh = 20000it is 500,000,000, and withh = 1000000000it is 10,000. The search works with these because the hours are along long. - Rounding down. Writing
pile / kloses the last partial hour of every pile. The result is a speed that is too slow. - Floating point.
ceil((double)pile / k)also works for these sizes, but the integer form(pile + k - 1) / kis simpler and exact. - Forgetting to store
k. Movingrighttok - 1without savingkloses the best speed found so far. - Starting
leftat 0. The speed 0 divides by zero inhours.
Testing
piles = {3, 6, 7, 11},h = 8gives 4.piles = {30, 11, 23, 4, 20},h = 5gives 30, andh = 6gives 23.piles = {5},h = 3gives 2.piles = {1, 1, 1},h = 3gives 1.piles = {1000000000},h = 2gives 500000000.piles = {312884470},h = 312884469gives 2.
