Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 6 of 30 minutes free
Free: The problem: sharing a textbook's chapters. With a pass: the other 10 sections and 3 interview answers, about 24 minutes.

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⁹.

  1. pages = [40, 25, 60, 15, 30, 20] and k = 3 give 65: the runs 40 + 25, 60 and 15 + 30 + 20.
  2. The same chapters with k = 1 give 190, every page.
  3. 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.

Trying every split, and how the number of splits grows Python · brute/every_split.py
"""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

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.

Chapters 40, 25, 60, 15, 30, 20 filled greedily: a limit of 65 gives 3 students, a limit of 64 gives 5.Limit 65: 3 students, shares 65, 60 and 65402565606015302065Limit 64: 5 students, two more than allowed4040252560601530452020If a limit works, every larger limit works too:search the limits from 60 to 190 for the smallest

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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.