Shipping capacity: search a feasible answer
Constructed practice problem; no company attribution. Prerequisites: binary search boundaries.
Candidate brief
A warehouse must ship packages in their listed order within a fixed number of days. Each day loads the next consecutive packages up to one common capacity. Find the smallest integer capacity that works. Why can you test a proposed capacity greedily?
Write this:
def shipping_capacity(weights, days):
...
| Contract | Decision |
|---|---|
| Input | List/tuple of positive integer weights and positive integer days; bool excluded |
| Output | Smallest capacity that ships all packages in order in at most days |
| Boundaries | Empty shipment gives 0; unused days allowed; packages cannot split |
| Invalid input | Invalid types, nonpositive weights, or nonpositive days raise ValueError |
| Excluded | Reordering, per-day capacity variation, and fractional package weights |
Optional refresher · the underlying tool
Search an answer when larger proposed answers can never become infeasible. Here a capacity C is feasible if the ordered packages can be split into at most the allowed number of days without exceeding C on a day.
weights = [3, 2, 4]
low, high = max(weights), sum(weights) # 4 and 9
For two days, capacity 5 works as [3,2] | [4]; capacity 4 does not. Prove the feasibility test is monotone before binary searching.
A design choice worth saying aloud
Extract a predicate named can_ship_with_capacity(capacity) whose only job is to count days while preserving package order. Test its boundary at 4 (false) and 5 (true) for [3,2,4] over two days before binary search. If the predicate cannot be shown monotone, binary search has no justification.
What the interviewer expects
The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.
Done means: Smallest capacity that ships all packages in order in at most days.
Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.
Test-case scenarios to settle before coding
01 · Representative
- Input / starting state
[3,2,2,4,1,4], days3- Expected result
6
What it is testing: Capacity 5 fails and 6 is feasible.
02 · Empty shipment
- Input / starting state
[], days2- Expected result
0
What it is testing: No capacity is required.
03 · One day
- Input / starting state
[2,3,4], days1- Expected result
9
What it is testing: Every package must fit in one ordered day.
04 · Many days
- Input / starting state
[2,3,4], days10- Expected result
4
What it is testing: Unused days are allowed; largest package is the floor.
05 · No splitting
- Input / starting state
[8,1,1], days2- Expected result
8
What it is testing: A package is indivisible.
06 · Invalid/atomic
- Input / starting state
- zero weight or nonpositive days
- Expected result
ValueError; input unchanged
What it is testing: Validate before feasibility search.
For each case, show which branch or state change produces that result.
shipping_capacity([3, 2, 2, 4, 1, 4], 3) == 6: days load [3, 2], [2, 4], [1, 4].
Capacity 5 needs four days, so 6 is minimal.
shipping_capacity([], 2) == 0; shipping_capacity([0, 3], 2) raises ValueError.
Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.
Worked lesson, changed requirements, and reference
Separate optimization from the yes/no test
A baseline tries capacities from the heaviest package through total weight,
simulating each until one works. For total S, maximum weight M, and n
packages, this costs O(n(S-M+1)) arithmetic operations. The repeated question is
“Can capacity C finish within D days?” If one capacity works, any larger one
also works by using the same grouping. Feasibility is monotone, so a binary
search can find its first true value.
Capacity for [3, 2, 2, 4, 1, 4] |
Greedy daily totals | Days needed | Feasible for D=3? |
|---|---|---|---|
| 5 | 5, 2, 5, 4 | 4 | no |
| 6 | 5, 6, 5 | 3 | yes |
| 8 | 7, 5, 4 | 3 | yes |
At a fixed capacity, load the next package if it fits; otherwise start a new day. The simulation invariant is that all prior packages are assigned in order and the current day's load never exceeds capacity. Its stronger optimality claim is that greedy ships at least as many prefix packages after each day as any feasible schedule at that capacity. On day one it takes the longest fitting prefix. If it is already at least as far after day k, filling the next day cannot leave it behind a competing schedule. Positive weights make the fitting-prefix argument valid. Thus if greedy needs too many days, no legal grouping can rescue C.
Search the inclusive answer interval [M, S]: M is necessary because a package
cannot split, and S always works in one day. A feasible midpoint moves the upper
bound to mid; an infeasible midpoint moves the lower bound to mid+1. The interval
always contains the minimum feasible capacity and strictly shrinks. Time is
O(n log(S-M+2)), including validation, and O(1) auxiliary space. The loop does
not allocate the daily groups because only capacity is requested. Large integer
arithmetic has additional bit-level cost beyond this operation count.
Follow-up 1: return a schedule with exactly D nonempty days
Predict the missing feasibility rule: for nonempty input this requires D <= n.
After finding capacity, greedily construct groups, but start a new group whenever
the remaining packages must each reserve one remaining day.
Input [2, 2, 2], capacity 4 |
At-most-D schedule | Exactly 3 nonempty days |
|---|---|---|
| Grouping | [2, 2], [2] |
[2], [2], [2] |
| Same capacity valid? | yes | yes, by splitting groups |
Splitting a group preserves the capacity bound because all weights are positive. Define the empty-shipment/exact-day policy separately. Output now costs O(n).
Follow-up 2: packages may be reordered
The old test can reject a capacity that works after reordering. Monotonicity of capacity remains, but the greedy feasibility oracle is no longer correct; exact packing may require exponential search or a bounded-size dynamic program. A senior candidate proves both monotonicity and oracle correctness independently. A lead candidate negotiates order, scale, and approximation policy before promising that binary search alone solves the changed optimization problem.
Run and check
From the repository root:
cd curriculum/01-code/02-data-structures-algorithms/problems/13-shipping-capacity
python -m unittest -v test_solution.py
Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.
def _integers(values):
if not isinstance(values, (list, tuple)) or any(type(x) is not int for x in values):
raise ValueError("expected a list or tuple of integers, excluding bool")
def shipping_capacity(weights, days):
_integers(weights)
if type(days) is not int or days <= 0 or any(weight <= 0 for weight in weights):
raise ValueError("days and weights must be positive integers")
if not weights:
return 0
lo, hi = max(weights), sum(weights)
while lo < hi:
capacity = lo + (hi - lo) // 2
used, load = 1, 0
for weight in weights:
if load + weight > capacity:
used += 1
load = 0
load += weight
if used <= days:
hi = capacity
else:
lo = capacity + 1
return lo
import itertools
import unittest
from solution import shipping_capacity
class ShippingTests(unittest.TestCase):
def test_contract(self):
self.assertEqual(shipping_capacity([3, 2, 2, 4, 1, 4], 3), 6)
self.assertEqual(shipping_capacity([], 2), 0)
self.assertEqual(shipping_capacity([5, 1], 20), 5)
self.assertEqual(shipping_capacity([5, 1], 1), 6)
for weights, days in [([0, 3], 2), ([1], 0), ([1], True), ([-1], 2), ([1.0], 2)]:
with self.assertRaises(ValueError):
shipping_capacity(weights, days)
def test_enumerated_partition_oracle(self):
for n in range(1, 6):
for weights in itertools.product(range(1, 4), repeat=n):
for days in range(1, n + 2):
candidates = []
for mask in range(1 << (n - 1)):
groups = []
start = 0
for i in range(n - 1):
if mask & (1 << i):
groups.append(sum(weights[start:i + 1]))
start = i + 1
groups.append(sum(weights[start:]))
if len(groups) <= days:
candidates.append(max(groups))
self.assertEqual(shipping_capacity(weights, days), min(candidates))
if __name__ == "__main__":
unittest.main()