The k lightest crates
List the k lightest crates in a grading run without sorting the run, using a heap built in one linear pass.
An orchard packhouse weighs every crate coming off the grader. The small van takes the three lightest, and the run holds a million crates.
The problem
Crates leave the grader in no particular weight order — the grader sorts by
size, and weight only correlates loosely with it. The dispatcher has a van going
to a single small shop and wants the k lightest crates in the run, listed by
increasing weight, so the loaders can pick them off in one walk down the line.
Two crates can weigh the same, and both count: the van takes crates, not
distinct weights. k may be zero, in which case nothing is loaded, and k may
exceed the number of crates, in which case the van takes the whole run and the
list is every weight in order.
Input. crates — a list of integers, the weight in kilograms of each crate
in grader order. k — how many crates the van takes.
Output. A list of the min(k, len(crates)) smallest weights, in increasing
order.
Example.
crates = [12, 5, 9, 5, 20, 3], k = 3 -> [3, 5, 5]
Both 5 kg crates are loaded; there is no de-duplication.
A second example, at the two ends of the range for k:
crates = [12, 5, 9, 5, 20, 3], k = 9 -> [3, 5, 5, 9, 12, 20]
crates = [12, 5, 9, 5, 20, 3], k = 0 -> []
Constraints.
0 <= len(crates) <= 10^61 <= crates[i] <= 10^40 <= k <= 10^6
Hints
Hint 1
Sorting answers it. When k is 3 and the run is a million crates, what are you
paying for that you then throw away?
Hint 2
There is an arrangement that knows only where the lightest crate is, and nothing else. What does it cost to build that arrangement from an unordered run?
Hint 3
Building a heap is not the same as pushing n items one at a time.
Approach
Brute force
Sort the run and slice the first k. That is about n log n comparisons — some
2·10⁷ for a million crates — and if k is 3, then 999 997 of the positions you
paid to order are discarded unread.
The insight
Only the first k positions need to be ordered, and a heap over the whole run
can be built in O(n) rather than O(n log n), so the total is O(n + k log n).
The linear build works because the tree is bottom-heavy: half the crates are
leaves and sift down zero levels, a quarter sift down at most one, an eighth at
most two, and the sum of those levels converges to n. It needs no precondition
at all — any arrangement of the run can be heapified, which is exactly why the
build is cheaper than inserting the crates one by one.
Algorithm
- If
kis zero or the run is empty, return the empty list. - Copy the weights, because heapify rearranges its argument in place.
- Heapify the copy in one pass.
- Pop
min(k, n)times, collecting each popped weight. - The pops already come out in increasing order.
Complexity
Time O(n + k log n) — a linear build plus k pops of log n each; for
k = 3 on a million crates that is a million steps rather than twenty million.
Space O(n) for the copy, or O(1) extra if rearranging the caller's list is
allowed.
Solution
"""The k lightest crates — heapify the whole run once, then pop k times."""
import heapq
def solve(crates, k):
if k <= 0:
return []
heap = list(crates) # copy: heapify rearranges its argument in place
heapq.heapify(heap) # O(n): half the nodes are leaves and sift down zero levels
k = min(k, len(heap))
# Invariant: after each pop the root is the lightest crate not yet listed,
# so the crates come out already in increasing order.
return [heapq.heappop(heap) for _ in range(k)]The cases that ran
TESTS = [
(([12, 5, 9, 5, 20, 3], 3), [3, 5, 5]),
(([12, 5, 9, 5, 20, 3], 6), [3, 5, 5, 9, 12, 20]),
(([12, 5, 9, 5, 20, 3], 9), [3, 5, 5, 9, 12, 20]),
(([12, 5, 9, 5, 20, 3], 0), []),
(([4, 4, 4, 4], 2), [4, 4]),
(([7], 1), [7]),
(([], 3), []),
]Pitfalls
- Heapifying the caller's list.
heapq.heapifyrearranges in place, so the dispatcher's grader-order record is destroyed and the next report reads a shuffled run. Copy first. - Reading
heap[:k]instead of popping. A heap is not sorted. After heapifying the first example the array is[3, 5, 9, 5, 20, 12], so the first three entries give[3, 5, 9]— and 9 kg is not among the three lightest. - Popping
ktimes without clamping. With six crates andk = 9, the seventh pop raisesIndexErroron an empty heap.
Variants
- The nearest drill bits — top-k
again, but the heap is capped at
kand keyed by a computed distance rather than by the value itself. - Top-k and two heaps — when the
size-
kheap beats heapifying everything, costed both ways.