763. Partition Labels

Partition a string into as many parts as possible so that each letter appears in at most one part. Two linear passes do it: first record every character's last occurrence, then sweep with a greedy window — extend the window whenever the current char's last index exceeds the window end, and cut as soon as you reach that end.

MediumGreedyTwo PointersHash MapTypeScript

PROBLEM What we're solving

Given a string s, partition it into as many substrings as possible so that each letter appears in exactly one part. Return a list of the partition sizes.

Example: s = "ababcbacadefegdehijhklij"
The answer is [9, 7, 8]. The partitions are "ababcbaca" (9), "defegde" (7), and "hijhklij" (8). Every letter in each piece never appears in any other piece.

KEY IDEA A partition must extend to the last occurrence of every char inside it

Insight → once you include any occurrence of character c, you must include every occurrence of c. So a partition starting at start must extend at least as far as last[c] for each character c it encounters. Scan left to right, grow the right boundary greedily, and cut the moment you arrive at that boundary — that is the earliest legal cut.

RECIPE Last-index map → greedy sweep

  • 0 · Precompute last indices. Scan s once and store last[c] = i for every character c at index i. Overwriting is fine — the final write is always the rightmost occurrence.
  • 1 · Initialize a window. Set start = 0, end = 0.
  • 2 · Sweep left to right. For each index i: end = Math.max(end, last[s[i]]). This ensures the current window covers all occurrences of every character seen so far.
  • 3 · Cut when i === end. We have just consumed every character that was forced into this partition. Record end − start + 1, then advance start = end + 1.
Classic confusion → people often reset end to start after a cut (instead of start = end + 1). The variable end naturally becomes the new start minus one at each cut, so the correct idiom is just start = end + 1; the next iteration of the loop immediately overwrites end via the Math.max.

COST Complexity & trade-offs

Naive: try every split point
O(n²)
Check all characters in each candidate piece.
Last-index + greedy sweep
O(n)
Two passes; O(1) space for fixed alphabet.

Space is O(1) for lowercase a–z (26-slot array) or O(k) for k distinct characters with a Map. The result array is O(p) where p is the number of partitions, which is at most O(n) but typically much smaller.

Pattern transfer → the same "last-occurrence window" idea appears in Merge Intervals(cut when the current segment doesn't overlap the next), Non-overlapping Intervals, and Jump Game II (extend the reachable range greedily and count jumps).

RUN IT Extend the window to last[ch]; cut when i == end

step 0 / 21
STARTPass 1 complete. Recorded the last occurrence of every character. Each char's last index determines how far its partition must extend.
1function partitionLabels(s: string): number[] {
2 // Pass 1: record the last index of every character
3 const last: Record<string, number> = {};
4 for (let i = 0; i < s.length; i++) {
5 last[s[i]] = i;
6 }
7
8 // Pass 2: sweep and cut when we reach the partition boundary
9 const result: number[] = [];
10 let start = 0;
11 let end = 0;
12
13 for (let i = 0; i < s.length; i++) {
14 end = Math.max(end, last[s[i]]); // stretch end to cover this char's last occurrence
15 if (i === end) { // we consumed everything this partition can reference
16 result.push(end - start + 1);
17 start = end + 1;
18 }
19 }
20 return result;
21}
a0b1a2b3c4b5a6c7a8d9e10f11e12g13d14e15h16
State
i
0
start
0
end
partitions
{ a:8, b:5, c:7, d:14, e:15, f:11, g:13, h:16 }
last
current characterinside current partitionfinalized partitionlast-occurrence map
slowfast

TYPESCRIPT The solution, annotated

partitionLabels.ts
function partitionLabels(s: string): number[] {
  // Pass 1: record the last index of every character
  const last: Record<string, number> = {};
  for (let i = 0; i < s.length; i++) {
    last[s[i]] = i;
  }

  // Pass 2: sweep and cut when we reach the partition boundary
  const result: number[] = [];
  let start = 0;
  let end = 0;

  for (let i = 0; i < s.length; i++) {
    end = Math.max(end, last[s[i]]); // stretch end to cover this char's last occurrence
    if (i === end) {                  // we consumed everything this partition can reference
      result.push(end - start + 1);
      start = end + 1;
    }
  }
  return result;
}

Reading it block by block

Lines 3–5 — build the last-index map.One linear scan stores each character's rightmost position in last. Because we overwrite on every visit, the final value is always the largest index for that character.
Lines 9–11 — initialize the greedy window. start is where the current partition begins. end is the minimum right boundary we must reach before we can cut — it starts equal to start.
Line 14 — extend the window. end = Math.max(end, last[s[i]])ensures the window stretches far enough to include every occurrence of the current character. This is the core greedy decision: we can never cut before last[s[i]].
Lines 15–18 — cut when we arrive at the boundary. When i === end, all characters in [start..end] have their last occurrences inside this range — a legal partition. Record its size and advance start.
Complexity → O(n) time: two linear passes over s. O(1) extra space for the 26-entry last map (or O(k) for k distinct chars with a Map).

INTERVIEWFollow-ups they'll ask

  • "Return the actual substrings, not their sizes?" Replace result.push(end - start + 1) with result.push(s.slice(start, end + 1)).
  • "What if characters can repeat across partitions?"That relaxes the constraint — all single-character partitions (of length 1 each) would be valid. The problem only has meaning with the "each char in at most one part" rule.
  • "Uppercase or mixed-case input?" Expand the last storage from 26 to 52 slots, or switch to a Map<string, number>.
  • "Merge Intervals relationship?" Each character defines an interval [first[c], last[c]]; the partitions are exactly the merged non-overlapping intervals of those character intervals, processed greedily from left to right.
  • "Can you do it in a single pass?" Not cleanly — you need the last index before deciding where to cut, which requires seeing the whole string first. The two-pass approach is optimal.

OPTIMAL Greedy

partitionLabels.ts
function partitionLabels(s: string): number[] {
  // Pass 1: record the last index of every character
  const last: Record<string, number> = {};
  for (let i = 0; i < s.length; i++) {
    last[s[i]] = i;
  }

  // Pass 2: sweep and cut when we reach the partition boundary
  const result: number[] = [];
  let start = 0;
  let end = 0;

  for (let i = 0; i < s.length; i++) {
    end = Math.max(end, last[s[i]]); // stretch end to cover this char's last occurrence
    if (i === end) {                  // we consumed everything this partition can reference
      result.push(end - start + 1);
      start = end + 1;
    }
  }
  return result;
}
Complexity → O(n) time: two linear passes over s. O(1) extra space for the 26-entry last map (or O(k) for k distinct chars with a Map).

ALT 1 Brute force — grow each partition by re-scanning

O(n²) time · O(1) space

Start a partition at start, then repeatedly extend its end: whenever a character inside [start..end] also appears somewhere later than end, push endout to that later index and re-scan. Cut once the window is stable.

approach-2.ts
function partitionLabels(s: string): number[] {
  const result: number[] = [];
  let start = 0;

  while (start < s.length) {
    let end = start;
    let i = start;
    // Keep widening [start..end] until no char inside it appears past end.
    while (i <= end) {
      // Does s[i] occur anywhere after the current end?
      for (let j = s.length - 1; j > end; j--) {
        if (s[j] === s[i]) {
          end = j;            // stretch the window to cover this occurrence
          break;
        }
      }
      i++;
    }
    result.push(end - start + 1);
    start = end + 1;
  }
  return result;
}
Note → Each partition repeatedly rescans the tail of the string for every character it contains, so building one partition is O(n) work and the total is O(n²). Precomputing every character's last index once collapses the inner search to an O(1) lookup, recovering the O(n) two-pass solution.

MNEMONIC The one-liner

"Every char drags its last twin into the same partition — cut only when no twin lives beyond."

TRIGGERS When you see ___ → reach for ___

"each letter in at most one part"last-index map + greedy window
partition / split string with character-range constraintlast[c] extension + cut at i===end
minimize number of partitions or maximize cutsgreedy interval merging
interval-merge / non-overlapping intervals variantsort by start, merge by extending end

SKELETON The reusable shape

skeleton.ts
const last: Record<string, number> = {};
for (let i = 0; i < s.length; i++) last[s[i]] = i;

const result: number[] = [];
let start = 0, end = 0;

for (let i = 0; i < s.length; i++) {
  end = Math.max(end, last[s[i]]);
  if (i === end) {
    result.push(end - start + 1);
    start = end + 1;
  }
}
return result;

FLASHCARDS Tap to flip

Why must a partition include all occurrences of a character?
The problem requires each letter to appear in exactly one part — any occurrence of c that lands outside the partition would violate that rule.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
What does the first pass compute for s = "ababcbacadefegdehijhklij"?
QUESTION 02
When does the algorithm cut (emit a partition)?
QUESTION 03
Trace the algorithm on s = "ababcbacadefegdehijhklij". What is the first partition size?
QUESTION 04
What is the time complexity of the two-pass greedy solution?
QUESTION 05
After a cut, how should start and end be updated?
QUESTION 06
For s = "eccbbbbdec", what does the algorithm return?
QUESTION 07
The Partition Labels approach is most similar to which other technique?
QUESTION 08
#763 · Partition LabelsRecord the last index of each character. Sweep left to right, extending the current partition boundary to max(last[c]); cut a new partition whenever the current index equals the boundary.Which algorithmic approach does this primarily use?
QUESTION 09
#763 · Partition LabelsRecord the last index of each character. Sweep left to right, extending the current partition boundary to max(last[c]); cut a new partition whenever the current index equals the boundary.Which implementation correctly solves it?