Given package weights in a fixed order, find the smallest ship capacity that delivers them all within D days
When you cannot construct the answer but can cheaply test a candidate, and that test is monotone, binary search the answer's numeric range instead of the input. The feasibility check here is greedy day-packing, giving O(n log S) time, and the whole implementation hinges on one loop invariant.
What the interviewer is scoring
- Does the candidate recognise the answer itself as the search space, without being nudged towards binary search
- Whether monotonicity of the feasibility predicate is argued rather than assumed
- That the low and high bounds are justified as facts about the problem, not picked as safe-looking numbers
- Whether exactly one branch of the loop excludes the midpoint, and they can say why that guarantees termination
- Does the candidate handle the overflow-safe midpoint and the ceiling-versus-floor pairing without prompting
Answer
Search the answer, not the array
Nothing here is sorted, and there is no array to bisect. What there is, instead, is a candidate answer you can test in linear time: given a capacity, simulate the loading and count the days. Ship capacity is an integer in a bounded range, and the test is cheap, so you can bisect the range of possible answers rather than trying to construct the answer directly. This is the technique candidates most often fail to spot, and the reason is that every binary search they have written before operated on an input collection.
The cues are consistent enough to learn. Any phrasing of the form "minimise the maximum", "maximise the minimum", or "smallest value such that everything fits" is a candidate. So is any problem where the answer is a bounded number, checking a number is easy, and constructing the optimum looks like it needs dynamic programming you cannot formulate.
The predicate has to be monotone
Define feasible(c) as "capacity c delivers everything within D days". Bisection is only valid if the predicate, read across increasing c, is false for a while and then true forever. Argue it rather than assuming it: raising the capacity never forces the packing to start a day earlier, so the day count is non-increasing in c, so once c is feasible every larger c is too. The predicate is F F F F T T T T and you are looking for the boundary — the first T.
The bounds come from the problem, not from caution. The lower bound is max(weights), because a capacity below the heaviest package can never load it at all. The upper bound is sum(weights), which ships everything in a single day and is therefore known feasible. That last point matters: because hi is feasible, the answer definitely exists inside [lo, hi], which is what lets the loop return without a final existence check.
private static int daysNeeded(int[] weights, int cap) {
int days = 1, load = 0;
for (int w : weights) {
if (load + w > cap) { days++; load = 0; } // order is fixed, so just start a new day
load += w;
}
return days;
}
That packing is greedy, and it is optimal because the order is fixed: leaving out a package that still fits today cannot possibly reduce the total number of days, since it only pushes work later. Worth saying out loud, because an interviewer who has seen this problem before will ask.
The loop and its invariant
int lo = 0, hi = 0;
for (int w : weights) { lo = Math.max(lo, w); hi += w; }
// Invariant: the answer is in [lo, hi]. Each iteration shrinks the range
// and never discards a value that could be the answer.
while (lo < hi) {
int mid = lo + (hi - lo) / 2; // overflow-safe, and mid < hi always
if (daysNeeded(weights, mid) <= D) hi = mid; // mid might be the answer: keep it
else lo = mid + 1; // mid is infeasible: drop it
}
return lo; // lo == hi: the first feasible capacity
Time is O(n · log(sum − max)) — one linear check per halving of the range — and space is constant. Note the shape of the complexity: the logarithm is over the magnitude of the answer, not over n, which is why this stays fast even when weights are large.
The five boundary conditions that make this bug-prone
These are the failures, in rough order of how often they appear on a whiteboard.
- Overflow.
(lo + hi) / 2overflows oncelo + hiexceeds the integer range, and the bug survives every small test case.lo + (hi - lo) / 2cannot overflow for non-negative bounds. - Progress. With floor division,
midcan equallobut never equalshi. Sohi = midalways shrinks the range, whilelo = midcan leave it unchanged and spin forever. If your logic genuinely needslo = mid, you must switch to a ceiling midpoint,lo + (hi - lo + 1) / 2. Floor pairs withhi = mid; ceiling pairs withlo = mid. Mixing them is the classic hang. - Exactly one branch excludes
mid. If both branches keep it the loop cannot terminate; if both exclude it you can step over the answer. Deciding which side ownsmidis the same decision as first-true versus last-false, and it should be settled before you write the comparison. - Existence.
while (lo < hi)returninglois only correct becausehiwas constructed to be feasible. Search a range where nothing may satisfy the predicate and you must test the returned value afterwards. - Off-by-one in the bound itself. Setting
loto0instead ofmax(weights)is harmless here because infeasible values are correctly rejected; settinghitoo low silently returns a wrong answer with no crash. Under-tight bounds cost logarithmic time, over-tight bounds cost correctness, so err loose.
Debug the invariant, not the comparison
The characteristic way this goes wrong in an interview is a candidate who writes the loop from memory, fails the sample, then starts perturbing it: < to <=, mid to mid + 1, lo to lo - 1, running the example again after each change. That process sometimes terminates in working code and it never terminates in an explanation, and interviewers read it as pattern recall with no model underneath.
The alternative costs one line of thought before any code: write down what the range means. Once you have committed to "the answer is in [lo, hi] and hi is known feasible", every branch is forced. mid feasible means the answer is at or below mid, so hi = mid. mid infeasible means the answer is strictly above, so lo = mid + 1. There is nothing left to guess, which is precisely why stating the invariant is worth more marks than the code it produces.
Say the invariant out loud before writing the loop, then derive each assignment from it. Candidates who tune comparison operators against the sample input are visibly guessing, and interviewers grade the difference.
Likely follow-ups
- Change the problem so packages may be reordered. Does the same approach survive?
- How does this change when the answer is a real number rather than an integer?
- Your feasibility check is itself greedy. Prove it computes the minimum number of days.
- When is a library binary search the wrong tool even though the data is sorted?
Related questions
- Find the length of the longest strictly increasing subsequence, then get it under quadratic time.hardAlso on binary-search6 min
- How do you know a greedy choice is safe and not merely plausible?mediumAlso on greedy4 min
- Merge a list of overlapping intervals, and justify the key you sorted byeasyAlso on invariants4 min
- Given a binary tree, decide whether it is a valid binary search tree.mediumAlso on invariants5 min
- How do you turn what the business tells you into a domain model that holds up once the exceptions arrive?hardAlso on invariants7 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumAlso on invariants6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: coding7 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: coding6 min