Problem overview
Koko has several piles of bananas and h hours. At speed k, she spends each hour on one pile and eats up to k bananas from it. A pile of p bananas therefore takes p / k hours rounded up, because a last, smaller bite still costs a whole hour. min_eating_speed takes piles: Vec<i32> and h: i32 and returns the smallest integer speed that finishes everything within h hours.
For piles = [3, 6, 7, 11] and h = 8, speed 4 takes 1 + 2 + 2 + 3 = 8 hours, which is just enough, and speed 3 takes 10 hours, which is too many. The answer is 4.
Approach: search the speeds, not the piles
The brute force is to try speed 1, then 2, then 3, and stop at the first one that works. That can take up to the size of the biggest pile in attempts, and each attempt reads every pile.
The key observation is that the speeds are ordered: if Koko finishes in time at speed k, she also finishes at any faster speed, and if she does not, no slower speed works either. So going from speed 1 upward, the speeds are "too slow" up to some point and "fast enough" from then on, with a single switch. Binary search finds that switch. It is a search on the answer: the thing being halved is the range of possible speeds.
The range is 1 to the biggest pile. At the biggest pile's speed, every pile takes one hour, and since h is never less than the number of piles, that speed always works. No answer can be larger.
For each candidate speed, a helper hours adds up the time of all the piles, which is O(n). With about log2(m) candidates, where m is the biggest pile, the total is O(n log m).
Step by step
- Set the range.
leftis 1, the slowest speed.rightis*piles.iter().max().unwrap(), the biggest pile.max()returns anOptionholding a reference (Nonefor an empty vector),unwrap()takes the reference out and*copies the number. - Start
answeratright. That speed is known to work, soansweris valid even before the search begins. - Loop while
left <= right. With<=, the last remaining speed is still tried. - Take the middle speed:
k = left + (right - left) / 2. - Ask whether
kis fast enough:Self::hours(&piles, k) <= h as i64. The&lends the vector instead of moving it into the call, andh as i64gives both sides of the comparison the same type. - If it is fast enough, store
kinanswerand look for something slower withright = k - 1. Store first: the search never returns tok, and it may be the smallest speed that works. - Otherwise
kand every slower speed are too slow, soleft = k + 1. - Return
answerwith no semicolon. When the loop ends, every speed belowanswerwas too slow. - Write the helper
hours. It borrows the piles as a slice&[i32], takes the speed, and returns ani64. It starts atotal: i64at 0 and loops over the piles with the pattern&pile, which copies each number out of its reference. - Round up with integers only. The hours for one pile are
(pile + k - 1) / k. A pile of 7 at speed 3 takes 3 hours, and(7 + 2) / 3is 3, while7 / 3alone is 2 and forgets the last bite. The result is turned into ani64withas i64before it is added.
The search for piles = [3, 6, 7, 11] and h = 8, printing each round:
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 takes 6 hours, which fits, so the answer is at most 6 and right becomes 5. Speed 3 takes 10 hours, which is too many, so left becomes 4. Speed 4 takes 8 hours and fits, so right becomes 3 and the loop ends with answer equal to 4.
The solution in Rust
impl Solution {
fn min_eating_speed(piles: Vec<i32>, h: i32) -> i32 {
let mut left = 1;
let mut right = *piles.iter().max().unwrap();
let mut answer = right;
while left <= right {
let k = left + (right - left) / 2;
if Self::hours(&piles, k) <= h as i64 {
answer = k;
right = k - 1;
} else {
left = k + 1;
}
}
answer
}
fn hours(piles: &[i32], k: i32) -> i64 {
let mut total: i64 = 0;
for &pile in piles {
total += ((pile + k - 1) / k) as i64;
}
total
}
}
The Rust details worth a closer look:
- Why
i64for the total. With 10,000 piles of 1,000,000,000 bananas, speed 1 takes 10,000,000,000,000 hours, far beyond what ani32holds (about 2.1 billion). The helper returns exactly that number. Ani32total would wrap silently in a-Obuild:10000 * 1_000_000_000ini32comes out as 1316134912. In a debug build the same overflow panics. - Why the pile can stay an
i32.pile + k - 1is at most about 2 billion, which still fits in ani32. Only the sum over many piles can get too big. - Slices.
&pilesis a&Vec<i32>, and Rust converts it to the&[i32]the helper asks for. The helper can read the numbers but not change or own them. for &pile in piles. Iterating over a slice yields references. The pattern&pilecopies each number out, sopileis ani32.- Associated function.
hourshas noself, so it is called asSelf::hours. - Types of
leftandright. Both are inferred asi32, sincerightcomes from the piles andkis passed tohours.
Complexity
Time: O(n log m). The search halves a range of at most m speeds, so it makes about log2(m) rounds, about 30 for a billion. Each round calls hours, which visits the n piles once.
Space: O(1). The search keeps a few integers and the helper borrows the piles.
Edge cases and common mistakes
- One pile.
[5]withh = 1gives 5: the whole pile must go in one hour. Withh = 5it gives 1. - As many hours as piles.
[30, 11, 23, 4, 20]withh = 5gives 30, the biggest pile, because every pile must be finished in one hour. - A little extra time. The same piles with
h = 6give 23. At speed 22 the pile of 23 needs 2 hours, so the total is 7, one too many. - Rounding down. Using
pile / kalone undercounts every pile that does not divide evenly, so slow speeds look faster than they are and the function returns too small an answer. - Overflow. Keep the total in an
i64. For the largest input, speed 1 needs 10 trillion hours. - Forgetting to save
answer. If you only moveright, you lose the working speed when the search then moves past it. - Using
left < right. When the range shrinks to one speed, that speed is never tested, so the search can miss the answer.
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 = [5],h = 1gives5, and withh = 5gives1.piles = [1000000000, 1000000000, 1000000000],h = 3gives1000000000.- 10,000 piles of 1,000,000,000 with
h = 1000000000gives10000.
