Facebook Pixel

2696. Minimum String Length After Removing Substrings

EasyStackStringSimulation
Leetcode Link

Problem Description

You have a string s that contains only uppercase English letters. You can perform operations on this string to make it shorter.

In each operation, you can find and remove either the substring "AB" or "CD" from anywhere in the string. When you remove a substring, the remaining parts of the string join together, which might create new "AB" or "CD" patterns that can be removed in subsequent operations.

For example, if you have the string "ACBDC" and remove "CD", you get "ACBC". Now the middle characters form "CB" which cannot be removed, but if there was an "AB" pattern after removal, you could remove that too.

Your task is to find the minimum possible length of the string after performing any number of these removal operations optimally.

The key insight is that after each removal, the string concatenates and might form new removable patterns. You need to keep removing "AB" and "CD" substrings until no more can be removed, and return the length of the final string.

The solution uses a stack-based approach where:

  • We traverse each character in the string
  • If the current character is 'B' and the top of stack is 'A', we can form "AB" and remove it (pop from stack)
  • If the current character is 'D' and the top of stack is 'C', we can form "CD" and remove it (pop from stack)
  • Otherwise, we push the current character onto the stack
  • The final stack size (minus the dummy element) represents the minimum string length
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The problem asks us to repeatedly remove "AB" and "CD" substrings until we can't remove any more. A naive approach would be to scan the string repeatedly, removing these patterns until no more exist. However, this could be inefficient as we might need multiple passes through the string.

The key observation is that when we remove a substring, the characters on either side come together. For instance, if we have "ACBD" and remove "AB" in the middle, we get "CD", which itself can be removed. This suggests we need a way to efficiently handle these cascading removals.

Think about processing the string character by character from left to right. When we see a character, we need to check if it can form a removable pattern with the character that came immediately before it. If we're currently looking at 'B', we need to know if the previous character was 'A'. Similarly, if we're looking at 'D', we need to know if the previous character was 'C'.

This naturally leads to a stack-based solution. As we process each character:

  • We maintain a stack of characters that couldn't be removed yet
  • When we encounter a new character, we check if it forms "AB" or "CD" with the top of the stack
  • If it does, we remove both characters (pop from stack and don't push the current character)
  • If it doesn't, we add the current character to our stack for future potential matches

The beauty of this approach is that it automatically handles cascading removals. When we pop a character from the stack after finding a match, the new top of the stack represents the character that now becomes adjacent to whatever comes next. This mimics exactly what happens when we remove a substring - the surrounding characters come together.

The characters remaining in the stack at the end are exactly those that couldn't be paired up and removed, giving us our minimum length string.

Learn more about Stack patterns.

Solution Approach

The solution uses a stack data structure to efficiently handle the removal of "AB" and "CD" substrings in a single pass through the string.

Implementation Details:

  1. Initialize the Stack: We start with a stack containing an empty string [""]. This dummy element serves as a guard to avoid checking if the stack is empty during traversal.

  2. Process Each Character: We iterate through each character c in the string s:

    for c in s:
  3. Check for Removable Patterns: For each character, we check if it forms a removable pattern with the top element of the stack:

    • If c == "B" and stk[-1] == "A", we found an "AB" pattern
    • If c == "D" and stk[-1] == "C", we found a "CD" pattern

    When either condition is true, we pop the stack (removing the first character of the pattern) and don't add the current character:

    if (c == "B" and stk[-1] == "A") or (c == "D" and stk[-1] == "C"):
        stk.pop()
  4. Add Non-Matching Characters: If the current character doesn't form a removable pattern with the stack's top, we push it onto the stack:

    else:
        stk.append(c)
  5. Return the Result: After processing all characters, the stack contains the dummy element plus all characters that couldn't be removed. The minimum length is:

    return len(stk) - 1

Why This Works:

  • The stack maintains characters in the order they would appear after all possible removals up to the current position
  • When we find a match, popping from the stack and not adding the current character effectively removes the pattern
  • The new top of the stack after popping becomes adjacent to the next character we process, automatically handling cascading removals
  • Time Complexity: O(n) where n is the length of the string - we process each character once
  • Space Complexity: O(n) in the worst case where no removals are possible

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 string "ACDABC" to see how the stack-based solution works:

Initial Setup:

  • Input string: "ACDABC"
  • Stack: [""] (starts with dummy element)

Step 1: Process 'A'

  • Current character: 'A'
  • Stack top: ""
  • Does 'A' form "AB" or "CD" with ""? No
  • Action: Push 'A' onto stack
  • Stack: ["", "A"]

Step 2: Process 'C'

  • Current character: 'C'
  • Stack top: "A"
  • Does 'C' form "AB" or "CD" with "A"? No
  • Action: Push 'C' onto stack
  • Stack: ["", "A", "C"]

Step 3: Process 'D'

  • Current character: 'D'
  • Stack top: "C"
  • Does 'D' form "CD" with "C"? Yes!
  • Action: Pop 'C' from stack (don't push 'D')
  • Stack: ["", "A"]

Step 4: Process 'A'

  • Current character: 'A'
  • Stack top: "A"
  • Does 'A' form "AB" or "CD" with "A"? No
  • Action: Push 'A' onto stack
  • Stack: ["", "A", "A"]

Step 5: Process 'B'

  • Current character: 'B'
  • Stack top: "A"
  • Does 'B' form "AB" with "A"? Yes!
  • Action: Pop 'A' from stack (don't push 'B')
  • Stack: ["", "A"]

Step 6: Process 'C'

  • Current character: 'C'
  • Stack top: "A"
  • Does 'C' form "AB" or "CD" with "A"? No
  • Action: Push 'C' onto stack
  • Stack: ["", "A", "C"]

Final Result:

  • Stack: ["", "A", "C"]
  • Minimum length = len(stack) - 1 = 3 - 1 = 2
  • The remaining string would be "AC" (which cannot be reduced further)

This example demonstrates how the stack approach automatically handles removals in a single pass. When we found "CD" at positions 2-3, we removed it immediately. Later, when we found "AB" at positions 4-5, we removed that too. The final stack contains only the characters that couldn't be paired up for removal.

Solution Implementation

1class Solution:
2    def minLength(self, s: str) -> int:
3        # Initialize stack with empty string as sentinel to avoid index errors
4        stack = [""]
5      
6        # Process each character in the input string
7        for char in s:
8            # Check if current character forms "AB" or "CD" pattern with stack top
9            if (char == "B" and stack[-1] == "A") or (char == "D" and stack[-1] == "C"):
10                # Remove the previous character as it forms a removable pair
11                stack.pop()
12            else:
13                # Add current character to stack if no pattern is formed
14                stack.append(char)
15      
16        # Return final length (subtract 1 for the sentinel empty string)
17        return len(stack) - 1
18
1class Solution {
2    /**
3     * Finds the minimum length of string after removing all occurrences of "AB" and "CD"
4     * @param s the input string to process
5     * @return the minimum possible length after all removals
6     */
7    public int minLength(String s) {
8        // Initialize a stack to track characters, with a sentinel value to avoid empty stack checks
9        Deque<Character> stack = new ArrayDeque<>();
10        stack.push(' '); // Sentinel character to prevent empty stack issues
11      
12        // Process each character in the string
13        for (char currentChar : s.toCharArray()) {
14            // Check if current character forms "AB" or "CD" with the top of stack
15            if ((currentChar == 'B' && stack.peek() == 'A') || 
16                (currentChar == 'D' && stack.peek() == 'C')) {
17                // Remove the matching character from stack (forms a removable pair)
18                stack.pop();
19            } else {
20                // Add current character to stack (doesn't form a removable pair)
21                stack.push(currentChar);
22            }
23        }
24      
25        // Return size minus 1 to exclude the sentinel character
26        return stack.size() - 1;
27    }
28}
29
1class Solution {
2public:
3    int minLength(string s) {
4        // Initialize stack with a dummy character to avoid empty stack checks
5        string stack = " ";
6      
7        // Process each character in the input string
8        for (char& c : s) {
9            // Check if current character forms a removable pair with stack top
10            // Remove "AB" pattern: if 'B' comes after 'A'
11            // Remove "CD" pattern: if 'D' comes after 'C'
12            if ((c == 'B' && stack.back() == 'A') || 
13                (c == 'D' && stack.back() == 'C')) {
14                // Remove the previous character as it forms a valid pair
15                stack.pop_back();
16            } else {
17                // Add current character to stack if no pair is formed
18                stack.push_back(c);
19            }
20        }
21      
22        // Return length minus 1 to exclude the dummy character
23        return stack.size() - 1;
24    }
25};
26
1/**
2 * Finds the minimum length of string after removing all "AB" and "CD" substrings
3 * @param s - Input string to process
4 * @returns The minimum length after all possible removals
5 */
6function minLength(s: string): number {
7    // Stack to track characters that cannot be removed
8    const stack: string[] = [];
9  
10    // Process each character in the input string
11    for (const character of s) {
12        // Check if current character can form "AB" or "CD" with top of stack
13        const topElement = stack.at(-1);
14        const formsAB = topElement === 'A' && character === 'B';
15        const formsCD = topElement === 'C' && character === 'D';
16      
17        if (formsAB || formsCD) {
18            // Remove the pair by popping from stack
19            stack.pop();
20        } else {
21            // Add character to stack if no pair can be formed
22            stack.push(character);
23        }
24    }
25  
26    // Return the number of remaining characters
27    return stack.length;
28}
29

Time and Space Complexity

Time Complexity: O(n), where n is the length of the string s.

The algorithm iterates through each character in the string exactly once. For each character, it performs constant-time operations:

  • Checking the condition (c == "B" and stk[-1] == "A") or (c == "D" and stk[-1] == "C") takes O(1) time
  • Either popping from the stack or appending to the stack takes O(1) time

Since we process each of the n characters exactly once with O(1) operations per character, the total time complexity is O(n).

Space Complexity: O(n), where n is the length of the string s.

The space complexity is determined by the stack stk. In the worst case, when no "AB" or "CD" pairs can be removed (for example, if the string contains only "A"s and "C"s, or characters in reverse order like "BDCA"), all characters from the input string will be pushed onto the stack. The stack starts with one empty string element and can grow to contain at most n + 1 elements (the initial empty string plus all n characters). Therefore, the space complexity is O(n).

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

Common Pitfalls

1. Forgetting to Handle Empty Stack

A common mistake is implementing the solution without a sentinel value, leading to index errors when checking stack[-1] on an empty stack.

Incorrect Implementation:

def minLength(self, s: str) -> int:
    stack = []  # No sentinel
  
    for char in s:
        # This will raise IndexError if stack is empty!
        if (char == "B" and stack[-1] == "A") or (char == "D" and stack[-1] == "C"):
            stack.pop()
        else:
            stack.append(char)
  
    return len(stack)

Solution: Always initialize the stack with a sentinel value (like an empty string) or add explicit empty stack checks:

# Option 1: Use sentinel (recommended)
stack = [""]

# Option 2: Add explicit checks
if stack and ((char == "B" and stack[-1] == "A") or (char == "D" and stack[-1] == "C")):
    stack.pop()

2. Attempting to Remove Patterns in Wrong Order

Some might try to remove only "AB" first, then "CD" in separate passes, missing opportunities for cascading removals.

Incorrect Approach:

def minLength(self, s: str) -> int:
    # Remove all AB first
    while "AB" in s:
        s = s.replace("AB", "")
    # Then remove all CD
    while "CD" in s:
        s = s.replace("CD", "")
    return len(s)

This fails for cases like "ACDBC" where removing "CD" first creates "ABC", allowing "AB" to be removed for a final result of "C" (length 1). The incorrect approach would return length 3.

Solution: Process characters in a single pass using the stack approach to handle removals as they become available.

3. Using String Concatenation Instead of Stack

Building the result string through concatenation is inefficient and harder to manage:

Inefficient Implementation:

def minLength(self, s: str) -> int:
    result = ""
  
    for char in s:
        if len(result) > 0 and ((char == "B" and result[-1] == "A") or 
                                (char == "D" and result[-1] == "C")):
            result = result[:-1]  # String slicing creates new string
        else:
            result += char  # String concatenation is O(n)
  
    return len(result)

Solution: Use a list/stack for O(1) append and pop operations instead of string manipulation.

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

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More