Coding Interview Patterns Module 27 – Greedy algorithms and proofs
Least-cost merges with a heap (Huffman)
Merge files or ropes at the least total cost: the problem and its limits, a brute force over every merge order, and the signal that ties it to Huffman codes.
What you will learn
- Merge items two at a time at the least total cost with a min-heap
- Prove that merging the two smallest first is optimal with an exchange argument
- Merge sorted sizes in O(n) with two queues instead of a heap
- Build a Huffman code from the same merges, give out its codes from lengths alone, and check it is prefix-free
- Extend the method to merges of k files at once and to merges of neighbouring files only
Before you start
What you’ll learn · Before you start
What you will learn
- Merge items two at a time at the least total cost with a min-heap
- Prove that merging the two smallest first is optimal with an exchange argument
- Merge sorted sizes in O(n) with two queues instead of a heap
- Build a Huffman code from the same merges, give out its codes from lengths alone, and check it is prefix-free
- Extend the method to merges of k files at once and to merges of neighbouring files only
Before you start
On this page
To merge files, or join ropes, at the least total cost when each merge costs the two sizes added together, always merge the two smallest that are left. A min-heap hands back the two smallest in O(log n) after every merge, so n items take O(n log n). The rule is the one David Huffman published in 1952 for codes with the least redundancy: with letter counts as the sizes, the merges build the code tree, and the total cost is the length of the coded text in bits. The problem is often called the minimum cost of ropes; here it is told with sorted log files.
Merging sorted log files
The problem
A server keeps n sorted log files of s[0], …, s[n − 1] megabytes. Merging two sorted files into one sorted file reads and writes every line of both, so it costs the sum of their sizes and gives a file of that size, which can be merged again. Merge everything into one file at the least total cost. Joining ropes is the same problem: joining two ropes costs the sum of their lengths. Limits: 1 ≤ n ≤ 10⁵ and 1 ≤ s[i] ≤ 10⁹.
[20, 4, 7, 9, 3]costs 87 at least: merge 3 and 4, then the two 7s, then 9 with 14 and last 20 with 23, for 7 + 14 + 23 + 43. Merging in the order given costs 138.[5, 5, 5, 5]costs 40: two pairs, then the two halves, for 10 + 10 + 20. Adding one file at a time to a growing file costs 45.[12]costs 0, because there is nothing to merge.
The brute force, and why it fails at these sizes
Try every pair at every step and keep the cheapest total. With k files left there are k(k − 1)/2 pairs to choose from, so n files have n!(n − 1)!/2ⁿ⁻¹ merge orders: 180 for the five files above, 2,571,912,000 for ten, and about 10^883,039 for 100,000.
# The brute force for merging files: at every step try every pair of the files that are left, merge it, and go on;
# keep the cheapest total. It is right by construction, and it counts the merge orders it tries.
from math import factorial, lgamma, log
def cheapest(sizes):
"""(the least total cost, the number of merge orders tried)"""
if len(sizes) <= 1:
return 0, 1
best, orders = None, 0
for i in range(len(sizes)):
for j in range(i + 1, len(sizes)):
rest = sizes[:i] + sizes[i + 1 : j] + sizes[j + 1 :] + [sizes[i] + sizes[j]]
cost, tried = cheapest(rest)
cost += sizes[i] + sizes[j]
orders += tried
best = cost if best is None else min(best, cost)
return best, orders
def in_given_order(sizes):
"""Merge the first two files, then the result with the next file, and so on."""
total, current = 0, sizes[0]
for s in sizes[1:]:
current += s
total += current
return total
for sizes in ([20, 4, 7, 9, 3], [5, 5, 5, 5], [12]):
best, orders = cheapest(sizes)
print(f"{sizes}: least cost {best} ({orders:,} merge order{'s' * (orders != 1)} tried); merged in the given order: {in_given_order(sizes)}")
for n in (10, 20):
print(f"{n} files: {factorial(n) * factorial(n - 1) // 2 ** (n - 1):,} merge orders") # C(n, 2) * C(n-1, 2) * ... * 1
n = 100_000
digits = (lgamma(n + 1) + lgamma(n) - (n - 1) * log(2)) / log(10)
print(f"{n:,} files: about 10^{digits:,.0f} merge orders") Output
[20, 4, 7, 9, 3]: least cost 87 (180 merge orders tried); merged in the given order: 138 [5, 5, 5, 5]: least cost 40 (18 merge orders tried); merged in the given order: 45 [12]: least cost 0 (1 merge order tried); merged in the given order: 0 10 files: 2,571,912,000 merge orders 20 files: 564,480,989,588,730,591,336,960,000,000 merge orders 100,000 files: about 10^883,039 merge orders
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
What to notice
A file is paid once per merge above it
Text description of the diagram
The diagram is a tree drawn from the bottom up. Squares are the five files of the first example, of 3, 4, 7, 9 and 20 MB; circles are the files the merges make. Arrows lead from the two parts of each merge to the file it makes.
- The 3 MB and 4 MB files merge into a 7 MB file.
- That file and the 7 MB file of the input merge into 14 MB.
- The 14 MB file and the 9 MB file merge into 23 MB.
- The 23 MB file and the 20 MB file merge into the final 43 MB file, drawn at the top with a double border.
Counting the merges above each square gives how often that file is copied: 4 times for the 3 MB and 4 MB files, 3 for the 7 MB file, 2 for the 9 MB file and once for the 20 MB file. The total cost is each size times that count, 3 × 4 + 4 × 4 + 7 × 3 + 9 × 2 + 20 × 1 = 87, the same as adding the four merges, 7 + 14 + 23 + 43.
- Every merge copies its two files once more. A file is paid for once for every merge above it in the tree, so the total is each size times the number of merges it goes through.
- So small files belong deep and large files high. Merging the two smallest first sends them through the most merges, and the largest file joins last, through one.
- The merged file joins the others. After every merge the two smallest have to be found again, among the files left and the new one.
A min-heap finds them in O(log n), for O(n log n) in all. When the sizes arrive sorted, two queues give the same total in O(n). Huffman codes come from the same merges, with letter counts as the sizes.
The heap, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why merging the two smallest is optimal: a proof
- A dry run of the heap
- Sorted sizes: two queues and O(n)
- Huffman codes from the same merges
- Codes from lengths alone, as DEFLATE builds them
- The code in five languages
- Merged sizes past 32 bits
- Edge cases, and five bugs that a search exposes
- Variants: k files at once, and neighbours only
- Exercise: the cheapest merges, then the lengths of a Huffman code
- The cheapest merges, then the lengths of a Huffman code
- A practice set with sample answers
- Check yourself
- Interview questions
- When merging files two at a time costs the two sizes added, why should the two smallest files be merged first?
- Prove that repeatedly merging the two smallest items gives the least total cost. Which property of merge trees does your proof rely on, and where would it break if a merge could take three items?
- Ten million file sizes arrive already sorted. Can you compute the least merge cost faster than with a heap? Give the method, why it is correct, and what you would measure to show it is faster.
- Log chunks must stay in time order, so only neighbouring chunks may be merged. A colleague merges the cheapest neighbouring pair each time. Is that optimal? If not, give a counterexample and a correct method with its cost.
- Key takeaways
10 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 24 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- A Method for the Construction of Minimum-Redundancy Codes (D. A. Huffman, Proceedings of the IRE 40(9), 1952) (Institute of Radio Engineers (IEEE))
- RFC 1951: DEFLATE Compressed Data Format Specification version 1.3, sections 3.2.2 and 3.2.7 (RFC Editor (IETF))
- RFC 1952: GZIP file format specification version 4.3 (RFC Editor (IETF))
- heapq: Heap queue algorithm (The Python Standard Library) (Python Software Foundation)
- collections.deque (The Python Standard Library) (Python Software Foundation)
- Java SE 25 API: PriorityQueue (Oracle)
- The Java Language Specification, Java SE 25: 15.18.2 Additive Operators (+ and -) for Numeric Types (Oracle)
- std::priority_queue (cppreference.com) (cppreference.com)
- Package container/heap (The Go Authors)
- ECMAScript language specification: Number.MAX_SAFE_INTEGER (Ecma International (TC39))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress