Facebook Pixel

3039. Apply Operations to Make String Empty

MediumArrayHash TableCountingSorting
Leetcode Link

Problem Description

You are given a string s consisting of lowercase English letters.

You need to repeatedly perform the following operation until the string becomes empty:

  • For every alphabet character from 'a' to 'z', remove the first occurrence of that character in s (if it exists).

This means in each operation, you scan through the alphabet from 'a' to 'z' and remove the first occurrence of each letter that appears in the string.

For example, with s = "aabcbbca":

  • First operation: Remove the first 'a' at position 0, first 'b' at position 3, and first 'c' at position 4. The string becomes "abbca".
  • Second operation: Remove the first 'a' at position 0, first 'b' at position 1, and first 'c' at position 3. The string becomes "ba".
  • Third operation: Remove the first 'a' at position 1 and first 'b' at position 0. The string becomes "" (empty).

Your task is to return the value of the string s right before applying the last operation that makes it empty. In the example above, the answer would be "ba" since that's the string before the final operation that empties it.

The key insight is that characters appearing more frequently in the original string will survive longer through the operations. The characters that appear the maximum number of times will be the ones remaining in the string right before it becomes empty. Among these characters, their relative order in the final string corresponds to their last occurrence positions in the original string.

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

Intuition

Let's think about what happens during each operation. We remove one occurrence of each unique character that exists in the string. This means that if a character appears n times in the original string, it will take exactly n operations to completely remove all occurrences of that character.

Consider which characters will be present in the string right before the final operation. Since each operation removes one occurrence of each existing character, the characters that survive the longest are those with the maximum frequency. If the maximum frequency is mx, then after mx - 1 operations, only the characters with frequency mx will remain (each having exactly one occurrence left).

For example, if we have s = "aabcbbca":

  • 'a' appears 3 times
  • 'b' appears 3 times
  • 'c' appears 2 times

After 2 operations, 'c' is completely removed, but 'a' and 'b' each have one occurrence remaining.

Now, which specific occurrences of these maximum-frequency characters survive? During each operation, we always remove the first occurrence of each character. This means the last occurrence of each character in the original string will be the one that survives the longest.

Therefore, the string right before the final operation consists of:

  1. Only the characters that appear with maximum frequency mx
  2. Specifically, the last occurrence of each such character in the original string
  3. These characters appear in their original relative order

To implement this, we need to:

  • Count the frequency of each character to find mx
  • Track the last position of each character in the original string
  • Build the result by including characters where both conditions are met: the character has maximum frequency AND we're at its last occurrence position

Learn more about Sorting patterns.

Solution Approach

The solution uses hash tables to efficiently track character frequencies and positions:

  1. Count character frequencies: Use a Counter (hash table) to count how many times each character appears in the string s. Store this in cnt.

  2. Find maximum frequency: Determine the maximum frequency mx among all characters. This can be done using cnt.most_common(1)[0][1], which returns the count of the most frequent character.

  3. Track last positions: Create a dictionary last where last[c] stores the index of the last occurrence of character c in the string. We iterate through the string with enumerate(s) and continuously update last[c] = i for each character. Since we process left to right, the final value will be the rightmost position.

  4. Build the result string: Iterate through the string one more time with index i and character c. For each position, check if:

    • The character c has maximum frequency: cnt[c] == mx
    • The current position is the last occurrence of this character: last[c] == i

    If both conditions are met, include this character in the result.

  5. Return the result: Join all selected characters to form the final string using "".join().

The time complexity is O(n) where n is the length of the string, as we make a few passes through the string. The space complexity is O(1) for the hash tables since we're limited to 26 lowercase letters, making the space usage constant regardless of input size.

This approach elegantly identifies exactly which characters and which specific occurrences form the string right before it becomes empty, without actually simulating all the removal operations.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the solution with s = "abacaba".

Step 1: Count character frequencies

  • 'a' appears 4 times
  • 'b' appears 2 times
  • 'c' appears 1 time

Step 2: Find maximum frequency

  • mx = 4 (the frequency of 'a')

Step 3: Track last positions Going through the string "abacaba" from left to right:

  • Index 0: 'a' β†’ last['a'] = 0
  • Index 1: 'b' β†’ last['b'] = 1
  • Index 2: 'a' β†’ last['a'] = 2
  • Index 3: 'c' β†’ last['c'] = 3
  • Index 4: 'a' β†’ last['a'] = 4
  • Index 5: 'b' β†’ last['b'] = 5
  • Index 6: 'a' β†’ last['a'] = 6

Final last positions: last = {'a': 6, 'b': 5, 'c': 3}

Step 4: Build the result string Now iterate through the string again and check each position:

  • Index 0: 'a', frequency is 4 (= mx), but last position is 6 (β‰  0) β†’ skip
  • Index 1: 'b', frequency is 2 (β‰  mx) β†’ skip
  • Index 2: 'a', frequency is 4 (= mx), but last position is 6 (β‰  2) β†’ skip
  • Index 3: 'c', frequency is 1 (β‰  mx) β†’ skip
  • Index 4: 'a', frequency is 4 (= mx), but last position is 6 (β‰  4) β†’ skip
  • Index 5: 'b', frequency is 2 (β‰  mx) β†’ skip
  • Index 6: 'a', frequency is 4 (= mx), and last position is 6 (= 6) β†’ include 'a'

Result: "a"

This makes sense because after 3 operations:

  • Operation 1: Remove first 'a', 'b', 'c' β†’ "aaba"
  • Operation 2: Remove first 'a', 'b' β†’ "aa"
  • Operation 3: Remove first 'a' β†’ "a"
  • Operation 4: Remove first 'a' β†’ "" (empty)

The string right before the last operation is indeed "a".

Solution Implementation

1from collections import Counter
2
3class Solution:
4    def lastNonEmptyString(self, s: str) -> str:
5        # Count the frequency of each character in the string
6        char_count = Counter(s)
7      
8        # Find the maximum frequency among all characters
9        max_frequency = char_count.most_common(1)[0][1]
10      
11        # Create a dictionary to store the last occurrence index of each character
12        last_occurrence = {char: index for index, char in enumerate(s)}
13      
14        # Build the result string by including characters that:
15        # 1. Have the maximum frequency
16        # 2. Are currently at their last occurrence position in the string
17        result = "".join(
18            char for index, char in enumerate(s) 
19            if char_count[char] == max_frequency and last_occurrence[char] == index
20        )
21      
22        return result
23
1class Solution {
2    public String lastNonEmptyString(String s) {
3        // Array to store frequency count for each letter (a-z)
4        int[] charFrequency = new int[26];
5      
6        // Array to store the last occurrence index for each letter
7        int[] lastOccurrenceIndex = new int[26];
8      
9        int stringLength = s.length();
10        int maxFrequency = 0;
11      
12        // First pass: Count frequency and track last occurrence of each character
13        for (int i = 0; i < stringLength; ++i) {
14            // Convert character to index (0-25 for a-z)
15            int charIndex = s.charAt(i) - 'a';
16          
17            // Increment frequency and update max frequency
18            charFrequency[charIndex]++;
19            maxFrequency = Math.max(maxFrequency, charFrequency[charIndex]);
20          
21            // Update last occurrence index for this character
22            lastOccurrenceIndex[charIndex] = i;
23        }
24      
25        // Build result string
26        StringBuilder result = new StringBuilder();
27      
28        // Second pass: Add characters with max frequency at their last occurrence
29        for (int i = 0; i < stringLength; ++i) {
30            int charIndex = s.charAt(i) - 'a';
31          
32            // Check if this character has max frequency AND this is its last occurrence
33            if (charFrequency[charIndex] == maxFrequency && lastOccurrenceIndex[charIndex] == i) {
34                result.append(s.charAt(i));
35            }
36        }
37      
38        return result.toString();
39    }
40}
41
1class Solution {
2public:
3    string lastNonEmptyString(string s) {
4        // Array to store frequency count for each letter (a-z)
5        int charFrequency[26]{};
6      
7        // Array to store the last occurrence index for each letter
8        int lastOccurrenceIndex[26]{};
9      
10        int stringLength = s.size();
11        int maxFrequency = 0;
12      
13        // First pass: Calculate frequency and last occurrence for each character
14        for (int i = 0; i < stringLength; ++i) {
15            int charIndex = s[i] - 'a';  // Convert character to 0-25 index
16          
17            // Update frequency and track maximum frequency
18            maxFrequency = max(maxFrequency, ++charFrequency[charIndex]);
19          
20            // Update last occurrence position for this character
21            lastOccurrenceIndex[charIndex] = i;
22        }
23      
24        string result;
25      
26        // Second pass: Build result string with characters having maximum frequency
27        // Include only the last occurrence of each max-frequency character
28        for (int i = 0; i < stringLength; ++i) {
29            int charIndex = s[i] - 'a';
30          
31            // Check if current character has max frequency AND this is its last occurrence
32            if (charFrequency[charIndex] == maxFrequency && 
33                lastOccurrenceIndex[charIndex] == i) {
34                result.push_back(s[i]);
35            }
36        }
37      
38        return result;
39    }
40};
41
1/**
2 * Returns the last non-empty string after repeatedly removing all occurrences
3 * of the most frequent character(s) from the string.
4 * 
5 * @param s - The input string containing only lowercase English letters
6 * @returns The final non-empty string after all removal operations
7 */
8function lastNonEmptyString(s: string): string {
9    // Array to count frequency of each letter (a-z)
10    const charFrequency: number[] = Array(26).fill(0);
11  
12    // Array to store the last occurrence index of each letter
13    const lastOccurrenceIndex: number[] = Array(26).fill(0);
14  
15    const stringLength: number = s.length;
16    let maxFrequency: number = 0;
17  
18    // First pass: count frequencies and track last occurrence indices
19    for (let i = 0; i < stringLength; ++i) {
20        // Convert character to index (0-25 for a-z)
21        const charIndex: number = s.charCodeAt(i) - 97;
22      
23        // Increment frequency and update max frequency
24        charFrequency[charIndex]++;
25        maxFrequency = Math.max(maxFrequency, charFrequency[charIndex]);
26      
27        // Update last occurrence index for this character
28        lastOccurrenceIndex[charIndex] = i;
29    }
30  
31    // Array to build the result string
32    const resultChars: string[] = [];
33  
34    // Second pass: collect characters that appear with max frequency
35    // and only at their last occurrence position
36    for (let i = 0; i < stringLength; ++i) {
37        const charIndex: number = s.charCodeAt(i) - 97;
38      
39        // Check if this character has max frequency and this is its last occurrence
40        if (charFrequency[charIndex] === maxFrequency && lastOccurrenceIndex[charIndex] === i) {
41            // Convert index back to character and add to result
42            resultChars.push(String.fromCharCode(charIndex + 97));
43        }
44    }
45  
46    return resultChars.join('');
47}
48

Time and Space Complexity

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

  • Creating the Counter takes O(n) time as it iterates through all characters in s
  • Finding the most common element using most_common(1) takes O(|Ξ£|) time where |Ξ£| is the size of the character set (26 lowercase letters), which is effectively O(1)
  • Building the last dictionary takes O(n) time as it iterates through all characters in s
  • The final list comprehension iterates through s once more with O(1) lookups for cnt[c] and last[c], taking O(n) time
  • The join operation takes O(k) where k is the length of the result string, and k ≀ n

Overall: O(n) + O(1) + O(n) + O(n) = O(n)

The space complexity is O(|Ξ£|), where |Ξ£| is the character set size (26 lowercase English letters). This is because:

  • The Counter cnt stores at most |Ξ£| unique characters and their counts, taking O(|Ξ£|) space
  • The last dictionary also stores at most |Ξ£| unique characters and their last indices, taking O(|Ξ£|) space
  • The output string takes O(k) space where k ≀ |Ξ£| (at most one occurrence of each character with maximum frequency)

Overall: O(|Ξ£|) + O(|Ξ£|) + O(|Ξ£|) = O(|Ξ£|)

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

Common Pitfalls

Pitfall 1: Misunderstanding Character Selection Logic

The Problem: A common mistake is thinking that we need ALL occurrences of characters with maximum frequency, rather than just their LAST occurrences. This leads to incorrect results.

Incorrect Approach:

# WRONG: This includes all occurrences of max-frequency characters
result = "".join(char for char in s if char_count[char] == max_frequency)

Why It's Wrong: If s = "aabcbbca", where 'a' and 'b' appear 3 times (max frequency), this would return "aabbba" instead of the correct "ba".

Correct Fix: Only include characters at their last occurrence position:

result = "".join(
    char for index, char in enumerate(s) 
    if char_count[char] == max_frequency and last_occurrence[char] == index
)

Pitfall 2: Dictionary Comprehension Overwrites Earlier Positions

The Problem: Using dictionary comprehension to find last occurrences works correctly, but beginners might try to find first occurrences and get confused:

# Attempting to find first occurrences (WRONG for this problem)
first_occurrence = {}
for i, c in enumerate(s):
    if c not in first_occurrence:
        first_occurrence[c] = i

Why It Matters: The problem requires the LAST occurrences of max-frequency characters to maintain proper ordering. Using first occurrences would give the wrong relative positions.

Correct Approach: The dictionary comprehension {char: index for index, char in enumerate(s)} naturally keeps the last occurrence because later iterations overwrite earlier values.

Pitfall 3: Forgetting Edge Cases

The Problem: Not handling edge cases like single-character strings or strings where all characters appear exactly once.

Example Edge Cases:

  • s = "a" β†’ Should return "a"
  • s = "abcdef" β†’ All characters have frequency 1, should return "abcdef"

Solution: The current implementation handles these correctly, but it's important to verify:

# Test with edge cases
assert Solution().lastNonEmptyString("a") == "a"
assert Solution().lastNonEmptyString("abc") == "abc"

Pitfall 4: Inefficient Maximum Frequency Calculation

The Problem: Some might iterate through all values to find the maximum:

# Less efficient approach
max_frequency = max(char_count.values())

Why the Given Solution is Better: Using char_count.most_common(1)[0][1] is more idiomatic and potentially more efficient for Counter objects, though both work correctly.

Alternative Correct Approach: If you prefer explicit max finding:

max_frequency = max(char_count.values()) if char_count else 0
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More