Coding Interview Patterns Module 9 – Binary search
Minimise the maximum: split into k groups
Split ordered work into k contiguous groups so that the largest group is as small as possible, with a greedy check, a binary search and a DP to compare.
What you will learn
- Split an ordered list into k contiguous groups so that the largest group sum is as small as possible
- Prove that filling each group greedily is optimal for a fixed limit
- State and handle the rules for k larger than n and for exactly k non-empty groups
- Compare the binary search with a dynamic programme for the same problem, and use the DP as a test oracle
Before you start
What you’ll learn · Before you start
What you will learn
- Split an ordered list into k contiguous groups so that the largest group sum is as small as possible
- Prove that filling each group greedily is optimal for a fixed limit
- State and handle the rules for k larger than n and for exactly k non-empty groups
- Compare the binary search with a dynamic programme for the same problem, and use the DP as a test oracle
Before you start
On this page
Minimising the maximum means splitting work so that whoever gets the most has as little as possible. When the work is an ordered list, such as the chapters of a book that must stay in order, and each of k people takes one unbroken run, the best split need not be found by trying splits. Guess a limit S and ask whether k people can cover the list without anyone going over S: one pass that fills each person in turn answers that, and a larger S never makes the answer worse. So the smallest workable S can be binary searched between the longest item and the total. Below are an original version of the problem, a measurement of how fast trying every split blows up, and the observation that makes the search possible.
The problem: sharing a textbook’s chapters
The problem
A study group of k students splits the n chapters of a textbook for revision. The chapters stay in order: each
student takes one unbroken run of chapters, every student takes at least one, and every chapter goes to exactly one
student. Chapter i has pages[i] pages. Return the smallest possible number of pages for the student who reads the
most, or −1 if no such split exists. Limits: 1 ≤ n ≤ 10⁵, 1 ≤ k ≤ 10⁵ and 1 ≤ pages[i] ≤ 10⁹.
pages = [40, 25, 60, 15, 30, 20]and k = 3 give 65: the runs 40 + 25, 60 and 15 + 30 + 20.- The same chapters with k = 1 give 190, every page.
pages = [10, 20]and k = 3 give −1: three students cannot each take one of two chapters.
Book allocation
In Indian interview preparation this task is widely known as the book allocation problem: books in a fixed order, each student takes a contiguous run, and the most pages anyone reads is to be as small as possible.
The brute force: try every split
Cutting n chapters into k runs means choosing k − 1 of the n − 1 gaps between chapters, so there are C(n − 1, k − 1) splits. Try them all and keep the best: a perfect judge for small inputs, and hopeless beyond them.
"""The smallest possible largest share, by trying every way to cut the chapters into k runs: the brute force."""
import math
import time
from itertools import accumulate, combinations
def smallest_largest_by_trying(pages, k):
n = len(pages)
if k > n:
return -1, 0
upto = [0, *accumulate(pages)] # upto[i] = pages of chapters 0 .. i - 1
best, splits = None, 0
for cuts in combinations(range(1, n), k - 1): # where students 2 .. k start
bounds = (0, *cuts, n)
largest = max(upto[b] - upto[a] for a, b in zip(bounds, bounds[1:]))
best = largest if best is None else min(best, largest)
splits += 1
return best, splits
for pages, k in [([40, 25, 60, 15, 30, 20], 3), ([40, 25, 60, 15, 30, 20], 1), ([10, 20], 3), ([40, 25, 60, 15, 30, 20], 6)]:
best, splits = smallest_largest_by_trying(pages, k)
print(f"k = {k}: {best} after {splits:,} split{'' if splits == 1 else 's'}")
big = [((7 * i) % 23) + 5 for i in range(28)] # 28 chapters of 5 to 27 pages
start = time.perf_counter()
best, splits = smallest_largest_by_trying(big, 8)
per_split = (time.perf_counter() - start) / splits
print(f"28 chapters, 8 students: {splits:,} splits, about {per_split * 1e6:.1f} microseconds each")
def duration(seconds):
for unit, size in (("year", 365 * 86_400), ("day", 86_400), ("hour", 3_600), ("minute", 60), ("second", 1)):
if seconds >= size or unit == "second":
value = round(seconds / size)
return f"about {value:,} {unit}{'' if value == 1 else 's'}"
for n, k in [(40, 10), (100, 10), (100_000, 50)]:
count = math.comb(n - 1, k - 1)
shown = f"{count:,} splits" if count < 10**15 else f"{len(str(count))}-digit number of splits"
print(f"{n:,} chapters, {k} students: {shown}, {duration(count * per_split) if count < 10**30 else 'far longer than the age of the universe'}") Output
k = 3: 65 after 10 splits k = 1: 190 after 1 split k = 3: -1 after 0 splits k = 6: 60 after 1 split 28 chapters, 8 students: 888,030 splits, about 1.4 microseconds each 40 chapters, 10 students: 211,915,132 splits, about 5 minutes 100 chapters, 10 students: 1,731,030,945,644 splits, about 28 days 100,000 chapters, 50 students: 183-digit number of splits, far longer than the age of the universe
This output changes from run to run: the times depend on the computer and on what else it is doing
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 every_split.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
Each split costs a microsecond or two, so 40 chapters among 10 students already take minutes, 100 chapters take weeks, and the stated limits are out of reach for any computer.
Permutation & Combination Calculator Count C(n − 1, k − 1), the splits of n chapters among k students, for sizes of your own.What to notice
Fix a limit S on the pages any one student may read. If k students can cover the chapters within S, they can within any larger limit too, so “k students are enough for limit S” is false for small limits and true from one limit on. And one pass answers it for a given S: give the first student chapters while they fit, then the next student, and count the students used.
The greedy fill at two limits
Text description of the diagram
The diagram shows the chapters of 40, 25, 60, 15, 30 and 20 pages as blocks whose widths follow their page counts, twice.
In the first row the limit is 65 pages. Filling each student in turn with as many chapters as fit gives three runs: 40 + 25 = 65, then 60, then 15 + 30 + 20 = 65. Three students are enough, with shares of 65, 60 and 65.
In the second row the limit is 64. The 25 no longer fits beside the 40, so the runs are 40, 25, 60, 15 + 30 = 45 and 20: five students are needed, more than three.
The note at the bottom says that if a limit works, every larger limit works too, so the limits from 60, the longest chapter, to 190, all the pages, can be searched for the smallest one that works.
The smallest workable limit by halving, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why filling greedily is optimal for a fixed limit: the proof
- At most k or exactly k: the rule that keeps every student busy
- The search in five languages, with 64-bit totals
- A dynamic programme for the same split, and 3,000 cases where both agree
- Who reads what: rebuilding the split
- Edge cases, variants and follow-ups
- Edge cases and common bugs
- Variants, with answers
- Follow-up questions, answered
- Practise: give every student their chapters
- Give every student their chapters
- Check yourself
- Interview questions
- To split an ordered list into the fewest contiguous groups whose sums are each at most S, you fill each group as far as it goes. Prove that this uses the fewest groups, then show where the proof breaks if items may be negative.
- The study group may now hand out chapters in any order: each student gets any set of chapters, and the most pages anyone reads should be as small as possible. Does the binary search still work? What would you do for 20 chapters, and for 10⁵?
- An O(n log S) binary search and an O(k × n²) dynamic programme both solve the ordered split into k groups. When would you choose the DP, how do you use each to test the other, and at what sizes does each stop being practical?
- Key takeaways
3 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 23 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
- itertools.combinations and itertools.accumulate (Python Software Foundation)
- math.comb (Python Software Foundation)
- Reducibility among Combinatorial Problems (Karp, 1972): PARTITION is NP-complete (Springer (Complexity of Computer Computations))
- The Java Language Specification, Java SE 25: 4.2.1 Integral Types and Values (Oracle)
- Go 1.1 Release Notes: the size of int on 64-bit platforms (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress