Facebook Pixel

763. Partition Labels

MediumGreedyHash TableTwo PointersString
Leetcode Link

Problem Description

You are given a string s and need to partition it into as many parts as possible with a special constraint: each letter must appear in at most one part.

For example, given the string "ababcc":

  • Valid partition: ["abab", "cc"] - letter 'a' only appears in the first part, 'b' only in the first part, and 'c' only in the second part
  • Invalid partition: ["aba", "bcc"] - letter 'b' appears in both parts
  • Invalid partition: ["ab", "ab", "cc"] - letters 'a' and 'b' appear in multiple parts

The key requirements are:

  1. Each letter can only appear in one partition
  2. You want to maximize the number of partitions (make each partition as small as possible)
  3. The partitions must maintain the original order - when concatenated back together, they should form the original string s
  4. Return a list of integers representing the size of each partition in order

For instance, if the string "ababcbacadefegdehijhklij" can be partitioned into ["ababcbaca", "defegde", "hijhklij"], the output would be [9, 7, 8] representing the lengths of each partition.

The challenge is to find where to make the cuts such that no letter crosses partition boundaries while creating as many partitions as possible.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that for each letter in the string, we need to know where it last appears. Why? Because once we include a letter in a partition, we must extend that partition at least until the last occurrence of that letter to ensure it doesn't appear in any other partition.

Think of it this way: if we see the letter 'a' at position 0, and 'a' also appears later at position 5, then our current partition must extend at least to position 5. But as we traverse from position 0 to 5, we might encounter other letters like 'b' or 'c' whose last occurrences are even further ahead - say position 8. This means our partition must now extend to position 8.

This suggests a greedy approach:

  1. Start a new partition from the current position
  2. As we traverse each character, keep track of the farthest position we must reach (based on the last occurrences of all characters we've seen so far)
  3. Once we reach that farthest position, we can safely end the current partition there, knowing all characters in this partition won't appear anywhere else
  4. Start a new partition from the next position

For example, with string "abaccb":

  • 'a' appears last at position 2
  • 'b' appears last at position 5
  • 'c' appears last at position 4

Starting at position 0 ('a'), we know we must go at least to position 2. As we traverse:

  • Position 0: 'a' requires going to position 2
  • Position 1: 'b' requires going to position 5 (update our target)
  • Position 2: 'a' requires position 2 (no change, still need position 5)
  • Position 3: 'c' requires position 4 (no change, still need position 5)
  • Position 4: 'c' requires position 4 (no change, still need position 5)
  • Position 5: 'b' requires position 5 (we've reached our target!)

So the entire string forms one partition of size 6, since we can't split it without breaking the constraint.

Learn more about Greedy and Two Pointers patterns.

Solution Approach

The implementation follows the greedy approach identified in our intuition:

Step 1: Build a mapping of last occurrences

last = {c: i for i, c in enumerate(s)}

We create a dictionary where each character maps to its last occurrence index in the string. By iterating through the string with enumerate(s), each character's position gets updated, so we naturally end up with the last position for each character.

Step 2: Traverse and partition greedily We maintain three variables:

  • mx: The farthest position we need to reach in the current partition (initially 0)
  • j: The starting index of the current partition (initially 0)
  • ans: List to store the sizes of each partition
for i, c in enumerate(s):
    mx = max(mx, last[c])
    if mx == i:
        ans.append(i - j + 1)
        j = i + 1

As we traverse each character at position i:

  1. Update the farthest position: mx = max(mx, last[c])

    • For the current character c, check where it last appears (last[c])
    • Update mx to be the maximum of the current mx and this last occurrence
    • This ensures our partition extends far enough to include all occurrences of every character we've seen
  2. Check if we can end the partition: if mx == i

    • When our current position i equals mx, it means we've reached the farthest position required for all characters in this partition
    • This is the optimal place to cut and start a new partition
  3. Record the partition size: ans.append(i - j + 1)

    • The size of the current partition is from index j to index i inclusive
    • Length = i - j + 1
  4. Start new partition: j = i + 1

    • Move the start of the next partition to the position right after the current partition ends

Time Complexity: O(n) where n is the length of the string - we traverse the string twice (once to build the last dictionary, once to partition)

Space Complexity: O(1) or O(26) for the last dictionary since there are at most 26 lowercase English letters

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the string "ababcbaca" step by step:

Step 1: Build the last occurrence map

  • Traverse the string to find where each letter last appears:
    • 'a' appears at positions 0, 2, 6, 8 โ†’ last['a'] = 8
    • 'b' appears at positions 1, 3, 5 โ†’ last['b'] = 5
    • 'c' appears at positions 4, 7 โ†’ last['c'] = 7

So last = {'a': 8, 'b': 5, 'c': 7}

Step 2: Traverse and partition

Initialize: mx = 0, j = 0, ans = []

PositionCharacterlast[char]mx (updated)mx == i?Action
0'a'8max(0, 8) = 88 โ‰  0Continue partition
1'b'5max(8, 5) = 88 โ‰  1Continue partition
2'a'8max(8, 8) = 88 โ‰  2Continue partition
3'b'5max(8, 5) = 88 โ‰  3Continue partition
4'c'7max(8, 7) = 88 โ‰  4Continue partition
5'b'5max(8, 5) = 88 โ‰  5Continue partition
6'a'8max(8, 8) = 88 โ‰  6Continue partition
7'c'7max(8, 7) = 88 โ‰  7Continue partition
8'a'8max(8, 8) = 88 = 8 โœ“End partition!

When we reach position 8:

  • mx == i (both are 8), so we can end the partition here
  • Partition size = i - j + 1 = 8 - 0 + 1 = 9
  • Add 9 to ans: ans = [9]
  • Update j = i + 1 = 9 (but we're done since i = 8 was the last character)

Result: [9] - The entire string forms one partition of size 9.

This makes sense because:

  • 'a' appears throughout the string (positions 0, 2, 6, 8)
  • We must include all occurrences of 'a' in the same partition
  • Since 'a' appears at the very end (position 8), the entire string must be one partition

Solution Implementation

1class Solution:
2    def partitionLabels(self, s: str) -> List[int]:
3        # Build a dictionary to store the last occurrence index of each character
4        last_occurrence = {char: index for index, char in enumerate(s)}
5      
6        # Initialize variables to track partition boundaries
7        max_last_index = 0  # Maximum last occurrence index in current partition
8        partition_start = 0  # Starting index of current partition
9        result = []
10      
11        # Iterate through each character in the string
12        for current_index, char in enumerate(s):
13            # Update the maximum last occurrence index for current partition
14            max_last_index = max(max_last_index, last_occurrence[char])
15          
16            # Check if we've reached the end of current partition
17            # (current index equals the furthest last occurrence of all chars seen)
18            if max_last_index == current_index:
19                # Calculate partition size and add to result
20                partition_size = current_index - partition_start + 1
21                result.append(partition_size)
22                # Update start position for next partition
23                partition_start = current_index + 1
24      
25        return result
26
1class Solution {
2    public List<Integer> partitionLabels(String s) {
3        // Array to store the last occurrence index of each letter (a-z)
4        int[] lastOccurrence = new int[26];
5        int stringLength = s.length();
6      
7        // First pass: Record the last index where each character appears
8        for (int i = 0; i < stringLength; ++i) {
9            lastOccurrence[s.charAt(i) - 'a'] = i;
10        }
11      
12        // List to store the sizes of each partition
13        List<Integer> partitionSizes = new ArrayList<>();
14      
15        // Variables to track the current partition
16        int maxReachInPartition = 0;  // Furthest index we need to include in current partition
17        int partitionStart = 0;        // Starting index of current partition
18      
19        // Second pass: Determine partition boundaries
20        for (int currentIndex = 0; currentIndex < stringLength; ++currentIndex) {
21            // Update the furthest index we need to reach in this partition
22            // to ensure all occurrences of encountered characters are included
23            maxReachInPartition = Math.max(maxReachInPartition, 
24                                          lastOccurrence[s.charAt(currentIndex) - 'a']);
25          
26            // If current index equals the furthest required index,
27            // we've found a complete partition
28            if (maxReachInPartition == currentIndex) {
29                // Add the size of this partition (inclusive of both ends)
30                partitionSizes.add(currentIndex - partitionStart + 1);
31                // Update start position for next partition
32                partitionStart = currentIndex + 1;
33            }
34        }
35      
36        return partitionSizes;
37    }
38}
39
1class Solution {
2public:
3    vector<int> partitionLabels(string s) {
4        // Array to store the last occurrence index of each character (a-z)
5        int lastOccurrence[26] = {0};
6        int stringLength = s.size();
7      
8        // First pass: record the last occurrence position of each character
9        for (int i = 0; i < stringLength; ++i) {
10            lastOccurrence[s[i] - 'a'] = i;
11        }
12      
13        // Vector to store the sizes of each partition
14        vector<int> partitionSizes;
15      
16        // Variables to track the current partition
17        int currentPartitionEnd = 0;    // The rightmost position needed for current partition
18        int partitionStart = 0;          // Starting index of current partition
19      
20        // Second pass: determine partition boundaries
21        for (int i = 0; i < stringLength; ++i) {
22            // Update the end of current partition based on the last occurrence
23            // of the current character
24            currentPartitionEnd = max(currentPartitionEnd, lastOccurrence[s[i] - 'a']);
25          
26            // If we've reached the end of the current partition
27            // (all characters in this partition don't appear later)
28            if (currentPartitionEnd == i) {
29                // Add the partition size to result
30                partitionSizes.push_back(i - partitionStart + 1);
31                // Update the start position for the next partition
32                partitionStart = i + 1;
33            }
34        }
35      
36        return partitionSizes;
37    }
38};
39
1/**
2 * Partitions a string into as many parts as possible so that each letter appears in at most one part.
3 * Returns an array of integers representing the size of these parts.
4 * 
5 * @param s - The input string to partition
6 * @returns An array of partition sizes
7 */
8function partitionLabels(s: string): number[] {
9    // Create an array to store the last occurrence index of each letter (a-z)
10    const lastOccurrence: number[] = Array(26).fill(0);
11  
12    // Helper function to convert a character to its corresponding array index (0-25)
13    const getCharIndex = (char: string): number => {
14        return char.charCodeAt(0) - 'a'.charCodeAt(0);
15    };
16  
17    const stringLength: number = s.length;
18  
19    // First pass: record the last occurrence index of each character
20    for (let i = 0; i < stringLength; i++) {
21        const charIndex: number = getCharIndex(s[i]);
22        lastOccurrence[charIndex] = i;
23    }
24  
25    // Array to store the sizes of each partition
26    const partitionSizes: number[] = [];
27  
28    // Second pass: determine the partitions
29    let partitionStart: number = 0;  // Start index of current partition
30    let partitionEnd: number = 0;    // Maximum end index for current partition
31  
32    for (let currentIndex = 0; currentIndex < stringLength; currentIndex++) {
33        // Update the partition end to include all occurrences of current character
34        const charIndex: number = getCharIndex(s[currentIndex]);
35        partitionEnd = Math.max(partitionEnd, lastOccurrence[charIndex]);
36      
37        // If we've reached the end of current partition
38        if (partitionEnd === currentIndex) {
39            // Calculate and store the partition size
40            const partitionSize: number = currentIndex - partitionStart + 1;
41            partitionSizes.push(partitionSize);
42          
43            // Move to the next partition
44            partitionStart = currentIndex + 1;
45        }
46    }
47  
48    return partitionSizes;
49}
50

Time and Space Complexity

The time complexity is O(n), where n is the length of the string s. This is because:

  • Building the last dictionary requires one pass through the string: O(n)
  • The second loop iterates through the string once more: O(n)
  • All operations inside the loop (max comparison, appending to list) are O(1)
  • Total: O(n) + O(n) = O(n)

The space complexity is O(|ฮฃ|), where |ฮฃ| is the size of the character set. In this problem, since the input consists of lowercase English letters, |ฮฃ| = 26. This is because:

  • The last dictionary stores at most one entry for each unique character in the string, which is bounded by the size of the alphabet: O(|ฮฃ|)
  • The ans list stores the partition sizes, but in the worst case has at most n elements. However, this is not counted as extra space beyond the output
  • Other variables (mx, j, i, c) use constant space: O(1)
  • Total: O(|ฮฃ|) = O(26) = O(1) for this specific problem with lowercase English letters

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Update the Maximum Last Index Continuously

A common mistake is to only check the last occurrence of the current character without maintaining the maximum across all characters seen so far in the current partition.

Incorrect approach:

for i, c in enumerate(s):
    # Wrong: Only considering current character's last occurrence
    if last[c] == i:
        ans.append(i - j + 1)
        j = i + 1

Why it fails: This would incorrectly partition when reaching a character's last occurrence, even if previous characters in the same partition extend further. For example, with "abba", it would incorrectly split after the first 'a' at index 0.

Correct approach:

for i, c in enumerate(s):
    # Continuously track the farthest position needed
    mx = max(mx, last[c])
    if mx == i:  # Only partition when we've covered all necessary positions
        ans.append(i - j + 1)
        j = i + 1

2. Off-by-One Error in Partition Size Calculation

Another frequent mistake is incorrectly calculating the partition size, often forgetting the inclusive nature of the boundaries.

Incorrect calculation:

ans.append(i - j)  # Wrong: Missing the +1

Correct calculation:

ans.append(i - j + 1)  # Correct: Inclusive of both start and end positions

For a partition from index 2 to index 5, the size should be 4 (indices 2, 3, 4, 5), which is 5 - 2 + 1 = 4, not 5 - 2 = 3.

3. Not Resetting the Partition Start Correctly

Incorrect reset:

j = i  # Wrong: Would overlap with previous partition

Correct reset:

j = i + 1  # Correct: Start next partition after current one ends

The next partition should start at the position immediately after the current partition ends, not at the ending position itself.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More