Coding Interview Patterns Module 27 – Greedy algorithms and proofs
Partition labels: cuts at last occurrences
Cut a string into the most parts with no letter in two of them, using a last-occurrence table and one scan, and see why it equals merging letter ranges.
What you will learn
- Partition a string into the most parts so that no letter appears in two parts
- Use a last-occurrence table to decide where a cut is safe
- Prove that the scan makes every safe cut and no other
- Connect the cut rule to merging the first-to-last range of every letter
Before you start
What you’ll learn · Before you start
What you will learn
- Partition a string into the most parts so that no letter appears in two parts
- Use a last-occurrence table to decide where a cut is safe
- Prove that the scan makes every safe cut and no other
- Connect the cut rule to merging the first-to-last range of every letter
Before you start
On this page
Partition labels asks for a string to be cut into as many pieces as possible so that no letter appears in more than one piece, and for the sizes of the pieces. The greedy answer reads the string twice. First record the last position of every letter; then scan from the left, stretching the current piece to the last position of each letter met in it, and cut the moment the scan reaches the piece’s end. Every cut is safe because each letter seen so far is finished, and no possible cut is missed, so the pieces are as many as they can be: O(n) time and 26 counters of memory. Merging, for every letter, the range from its first to its last position gives the same pieces.
The problem: one rider per area
The problem
A courier’s route for the day is a string with one lowercase letter per stop, the area code of that stop, in the
order of delivery. The dispatcher hands the route to several riders as consecutive legs, and each area must be
served by one rider only: no letter may appear in two legs. Split the route into as many legs as possible and
return their sizes in order. Limits: 1 ≤ n ≤ 10⁶, and only the letters a to z appear.
"mnmopqpqrssr"gives[3, 1, 4, 4]: the legsmnm,o,pqpqandrssr, as in the figure."abcd"gives[1, 1, 1, 1]: every stop is an area of its own."abca"gives[4]: the first and the last stop share an area, so the route cannot be cut at all.
The first example and its four legs
Text description of the diagram
The figure shows the day's route as twelve boxes, the stops 0 to 11, each holding its area code: m, n, m, o, p, q, p, q, r, s, s, r. Dashed lines mark three cuts, after stops 2, 3 and 7, and brackets under the boxes mark the four legs they make:
- stops 0 to 2, "mnm", a leg of 3;
- stop 3, "o", a leg of 1;
- stops 4 to 7, "pqpq", a leg of 4;
- stops 8 to 11, "rssr", a leg of 4.
No letter appears in two legs, and no further cut is possible: any other cut would separate two stops of one area.
The brute force, and why it fails at these sizes
For every place to cut, compare the set of areas on its left with the set on its right, and make every cut where the two share nothing:
# A courier's day: one letter per stop, the area code of the stop, in the order of delivery.
DAY = "mnmopqpqrssr"
def legs_by_checking_every_cut(route):
"""Checks every place to cut: a cut is allowed when no area code appears on both sides. Returns (sizes, letters read)."""
read, cuts = 0, []
for i in range(len(route) - 1): # a cut between stop i and stop i + 1
left, right = set(route[: i + 1]), set(route[i + 1 :])
read += len(route)
if not left & right:
cuts.append(i)
edges = [-1, *cuts, len(route) - 1]
return [b - a for a, b in zip(edges, edges[1:])], read
print("the day", DAY, "->", legs_by_checking_every_cut(DAY))
print("abcd ->", legs_by_checking_every_cut("abcd"), " abca ->", legs_by_checking_every_cut("abca"))
for n in (1_000, 2_000, 4_000):
route = "".join("abcdefghij"[(i * 7) % 10] for i in range(n))
print(f"{n:,} stops: {legs_by_checking_every_cut(route)[1]:,} letters read") Output
the day mnmopqpqrssr -> ([3, 1, 4, 4], 132) abcd -> ([1, 1, 1, 1], 12) abca -> ([4], 12) 1,000 stops: 999,000 letters read 2,000 stops: 3,998,000 letters read 4,000 stops: 15,996,000 letters read
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 every_cut.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 cut reads the whole route once, so a route of n stops costs about n² letters read: 999,000 for 1,000 stops, 15,996,000 for 4,000 and about 10¹² for a million, which no time limit allows.
What to notice
- A cut after a stop is allowed exactly when every area seen so far has had its last stop. So the last stop of each area is the only fact about the rest of the route that matters.
- Allowed cuts never get in each other’s way. Making one does not stop another, so the most legs come from making every allowed cut.
The scan with a last-stop table, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why every cut is allowed and none is missed: the proof
- A dry run on the day
- The same answer as merged spans
- The code in five languages
- Checked against every cut on 2,000 random routes
- Four bugs and the smallest routes that break them
- Edge cases
- Variants
- Exercise: split the route
- Split a delivery route so no area is served twice
- A practice set with sample answers
- Check yourself
- Interview questions
- In partition labels, why can you make every allowed cut at once, instead of having to choose between allowed cuts?
- The route now arrives one stop at a time from a live feed, and the dispatcher wants each leg announced as soon as it is certain. When can a leg be announced, what must you store, and what changes if each area's number of stops is known in advance?
- The area codes are now postal codes of six digits, and a day's file holds 50 million stops with about 20,000 distinct codes. How do you compute the legs within a memory limit of 1 GB, and what do time and memory come to?
- The rule changes: an area may now be served in several legs, but no leg may visit the same area twice. What is the fewest number of legs, why is your greedy optimal, and how does it differ from partition labels?
- Key takeaways
1 diagram · 6 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 20 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
- greedy algorithm (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- Mapping types: dict (The Python Standard Library), insertion order (Python Software Foundation)
- Time complexity of operations on built-in types (dict and str) (Python Software Foundation)
- ECMAScript Language Specification: String.prototype.charCodeAt (Ecma International (TC39))
- Java SE 25 API: String.charAt (Oracle)
- std::basic_string_view (C++ reference) (cppreference.com)
- The Go Programming Language Specification: string types and index expressions (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress