Example 1
ready- Input
s = "ababcbacadefegdehijhklij"- Expected
[ 9, 7, 8 ]- Why
- each cut waits until every letter in its part has finished
Imagine filing a message into folders with one strict rule: a letter may belong to only one folder. Your job is to create as many folders as the message allows.
Given a lowercase string s, split it into as many non-empty contiguous parts, blocks
made from neighboring characters, as possible so that every distinct letter appears in
at most one part. Return the sizes of those parts from left to right.
You must use every character exactly once and preserve the original order. A letter may appear many times inside its one part; it just can't cross a boundary.
1 <= s.length <= 10_000s contains only lowercase English letters a through z.Hints
Record the first and last position of every letter. A part containing that letter must cover its entire span.
Start a part and set its tentative end to the current letter's last occurrence. Every letter you meet before that end may push the end farther right.
When your index reaches the furthest last occurrence seen inside the current part, none of its letters appears later. Cut there immediately to leave the most room for future parts.
Could you return the actual substrings instead of their sizes without changing the asymptotic complexity?
Visible cases
s = "ababcbacadefegdehijhklij"[
9,
7,
8
]s = "eccbbbbdec"[
10
]s = "abaccbdeffed"[
6,
6
]Interview signal
Every letter draws an invisible span across the string. Valid cuts can only live between those spans.
An interval is the span from one position through another, including both ends. Find each letter's first and last position. Sort those intervals by their first position, then merge overlaps. Every merged interval is one forced part: splitting inside it would place some letter on both sides.
def partition_labels(s: str) -> list[int]:
first: dict[str, int] = {}
last: dict[str, int] = {}
for i, letter in enumerate(s):
if letter not in first:
first[letter] = i
last[letter] = i
spans = sorted((first[letter], last[letter]) for letter in first)
part_start, part_end = spans[0]
sizes: list[int] = []
for span_start, span_end in spans[1:]:
if span_start > part_end:
sizes.append(part_end - part_start + 1)
part_start, part_end = span_start, span_end
else:
part_end = max(part_end, span_end)
sizes.append(part_end - part_start + 1)
return sizesTime O(n) — scanning dominates because there are at most 26 spans to sort. Space O(1) — the alphabet bounds the maps and span list to 26 entries.
You don't need to build the intervals explicitly. Record only each letter's last index,
then sweep left to right. part_end is the furthest last occurrence belonging to the
current part. When the scan reaches it, the part is closed and safe to emit.
def partition_labels(s: str) -> list[int]:
last = {letter: i for i, letter in enumerate(s)}
sizes: list[int] = []
part_start = 0
part_end = 0
for i, letter in enumerate(s):
part_end = max(part_end, last[letter])
if i == part_end:
sizes.append(part_end - part_start + 1)
part_start = i + 1
return sizesThe exchange argument is a proof that replacing another valid choice with the greedy
choice can't make the final result worse. Once a part starts, every letter encountered
forces its last occurrence into that part, so no valid first cut can come before
part_end. When the scan reaches part_end, cutting there is safe. Exchange any later
first cut in an optimal partition for this earliest safe cut: the first part gets no
invalid split, and the untouched suffix has at least as many chances to be partitioned.
Making that earliest safe cut repeatedly is therefore globally optimal for maximizing
the number of parts.
Time O(n) — one pass records last positions and one pass forms parts. Space O(1) because at most 26 last positions are stored.
Ready to run.
3 cases are queued.
Visible testcase
Case 1
Expected
[ 9, 7, 8 ]