Facebook Pixel

1003. Check If Word Is Valid After Substitutions

MediumStackString
Leetcode Link

Problem Description

You are given a string s and need to determine if it is valid according to specific rules.

A string s is considered valid if you can build it starting from an empty string t = "" by repeatedly performing this operation:

  • Insert the string "abc" at any position in t

In other words, you can place "abc" anywhere in your current string - at the beginning, end, or middle. The string t becomes t_left + "abc" + t_right, where t_left and t_right are the parts before and after the insertion point (either or both can be empty).

Your task is to return true if the given string s can be formed this way, otherwise return false.

For example:

  • "abc" is valid (insert "abc" into empty string once)
  • "aabcbc" is valid (insert "abc" into empty string to get "abc", then insert another "abc" at position 1 to get "aabcbc")
  • "abcabc" is valid (insert "abc" twice)
  • "abccba" is invalid (cannot be formed by inserting "abc" patterns)

The solution uses a stack-based approach. Since we're only adding "abc" patterns, if we can remove all "abc" substrings from s and end up with an empty string, then s is valid. The code pushes each character onto a stack and checks if the last three characters form "abc" - if so, it removes them. If the stack is empty after processing all characters, the string is valid.

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

Intuition

The key insight is to think about this problem in reverse. Instead of trying to build the string s by inserting "abc" patterns, we can verify if s is valid by removing "abc" patterns from it.

Consider what happens when we build a valid string: we start with an empty string and keep inserting "abc" at various positions. If we reverse this process, we should be able to remove these "abc" patterns one by one and eventually get back to an empty string.

Think of it like a parentheses matching problem. When we encounter a complete "abc" pattern, we can immediately remove it because it represents one valid insertion operation. The beauty of using a stack is that it naturally handles nested or overlapping patterns.

For example, if we have "aabcbc":

  • We process characters one by one: a, a, b, c - at this point, the last three characters form "abc", so we remove them
  • We continue with b, c - now our stack has a, b, c which again forms "abc", so we remove them
  • The stack is empty, confirming the string is valid

This approach works because:

  1. Each valid string must have a length that's a multiple of 3 (since we only add 3 characters at a time)
  2. At any point during the removal process, finding an "abc" pattern means we've identified one insertion operation
  3. If we can't reduce the string to empty by removing "abc" patterns, then the string couldn't have been built by only inserting "abc" patterns

The stack naturally maintains the order of characters and makes it easy to check and remove the most recent three characters when they form "abc".

Learn more about Stack patterns.

Solution Approach

The implementation uses a stack-based approach to validate the string. Here's how the algorithm works step by step:

1. Length Check: First, we check if the length of string s is a multiple of 3:

if len(s) % 3:
    return False

Since we can only insert "abc" (3 characters) at a time, any valid string must have a length divisible by 3. If not, we immediately return false.

2. Stack Processing: We initialize an empty list t to serve as our stack:

t = []

3. Character-by-Character Traversal: We iterate through each character c in the string s:

for c in s:
    t.append(c)

For each character, we push it onto the stack.

4. Pattern Detection and Removal: After adding each character, we check if the last three elements in the stack form "abc":

if ''.join(t[-3:]) == 'abc':
    t[-3:] = []
  • t[-3:] gets the last three elements from the stack
  • ''.join(t[-3:]) concatenates them into a string
  • If this string equals "abc", we remove these three elements by setting t[-3:] to an empty list

5. Final Validation: After processing all characters, we check if the stack is empty:

return not t

An empty stack means all characters were successfully grouped into "abc" patterns and removed, confirming the string is valid.

Time Complexity: O(n) where n is the length of the string, as we traverse the string once.

Space Complexity: O(n) in the worst case where no "abc" patterns can be removed, causing all characters to remain in the stack.

The algorithm essentially simulates the reverse of the string construction process - if we can decompose the string back to empty by removing "abc" patterns, then it must have been built using only "abc" insertions.

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 the example s = "aabcbc":

Initial Check:

  • Length of s is 6, which is divisible by 3 ✓
  • Initialize empty stack t = []

Step-by-step processing:

  1. Process 'a' (index 0):

    • Push 'a' onto stack: t = ['a']
    • Check last 3 elements: only 1 element, no action
  2. Process 'a' (index 1):

    • Push 'a' onto stack: t = ['a', 'a']
    • Check last 3 elements: only 2 elements, no action
  3. Process 'b' (index 2):

    • Push 'b' onto stack: t = ['a', 'a', 'b']
    • Check last 3 elements: 'aab' ≠ 'abc', no removal
  4. Process 'c' (index 3):

    • Push 'c' onto stack: t = ['a', 'a', 'b', 'c']
    • Check last 3 elements: 'abc' == 'abc'
    • Remove last 3 elements: t = ['a']
  5. Process 'b' (index 4):

    • Push 'b' onto stack: t = ['a', 'b']
    • Check last 3 elements: only 2 elements, no action
  6. Process 'c' (index 5):

    • Push 'c' onto stack: t = ['a', 'b', 'c']
    • Check last 3 elements: 'abc' == 'abc'
    • Remove last 3 elements: t = []

Final Check:

  • Stack is empty: t = []
  • Return true - the string is valid!

This example shows how the algorithm identifies and removes two "abc" patterns: one at positions 1-3 (after processing the 4th character) and another formed by the remaining characters, confirming that "aabcbc" can be built by inserting "abc" patterns.

Solution Implementation

1class Solution:
2    def isValid(self, s: str) -> bool:
3        # If string length is not divisible by 3, it cannot be valid
4        # since we need to remove "abc" sequences which are 3 characters each
5        if len(s) % 3 != 0:
6            return False
7      
8        # Use a stack to track characters
9        stack = []
10      
11        # Process each character in the string
12        for char in s:
13            # Add current character to stack
14            stack.append(char)
15          
16            # Check if the last 3 characters form "abc"
17            # If so, remove them from the stack
18            if ''.join(stack[-3:]) == 'abc':
19                stack[-3:] = []
20      
21        # String is valid if stack is empty after processing
22        # (all "abc" sequences have been successfully removed)
23        return len(stack) == 0
24
1class Solution {
2    /**
3     * Validates if a string is valid by repeatedly removing "abc" substrings.
4     * A string is valid if it becomes empty after all possible "abc" removals.
5     * 
6     * @param s the input string to validate
7     * @return true if the string is valid, false otherwise
8     */
9    public boolean isValid(String s) {
10        // Early optimization: if string length is not divisible by 3,
11        // it cannot be formed by "abc" patterns
12        if (s.length() % 3 != 0) {
13            return false;
14        }
15      
16        // Use StringBuilder as a stack to build and remove characters
17        StringBuilder stack = new StringBuilder();
18      
19        // Process each character in the input string
20        for (char currentChar : s.toCharArray()) {
21            // Add current character to the stack
22            stack.append(currentChar);
23          
24            // Check if the last 3 characters form "abc"
25            int stackLength = stack.length();
26            if (stackLength >= 3) {
27                // Extract the last 3 characters
28                String lastThreeChars = stack.substring(stackLength - 3);
29              
30                // If they form "abc", remove them from the stack
31                if ("abc".equals(lastThreeChars)) {
32                    stack.delete(stackLength - 3, stackLength);
33                }
34            }
35        }
36      
37        // String is valid if the stack is empty after processing
38        return stack.length() == 0;
39    }
40}
41
1class Solution {
2public:
3    bool isValid(string s) {
4        // Early return if string length is not divisible by 3
5        // Since "abc" has length 3, valid strings must have length that's multiple of 3
6        if (s.size() % 3 != 0) {
7            return false;
8        }
9      
10        // Use a stack-like string to process characters
11        string stack;
12      
13        // Process each character in the input string
14        for (char c : s) {
15            // Add current character to the stack
16            stack.push_back(c);
17          
18            // Check if the last 3 characters form "abc"
19            if (stack.size() >= 3 && 
20                stack.substr(stack.size() - 3, 3) == "abc") {
21                // Remove "abc" from the stack
22                stack.erase(stack.end() - 3, stack.end());
23            }
24        }
25      
26        // String is valid if stack is empty after processing
27        return stack.empty();
28    }
29};
30
1/**
2 * Validates if a string can be reduced to empty by repeatedly removing "abc" substrings
3 * @param s - The input string to validate
4 * @returns true if the string is valid (can be completely reduced), false otherwise
5 */
6function isValid(s: string): boolean {
7    // Early optimization: if length is not divisible by 3, it cannot be valid
8    // since each "abc" removal reduces length by 3
9    if (s.length % 3 !== 0) {
10        return false;
11    }
12  
13    // Stack to track characters as we process the string
14    const stack: string[] = [];
15  
16    // Process each character in the string
17    for (const char of s) {
18        // Add current character to stack
19        stack.push(char);
20      
21        // Check if the last 3 characters form "abc"
22        if (stack.slice(-3).join('') === 'abc') {
23            // Remove the last 3 characters ("abc") from stack
24            stack.splice(-3);
25        }
26    }
27  
28    // String is valid if stack is empty (all characters were removed)
29    return stack.length === 0;
30}
31

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 using a for loop. Within each iteration, it performs the following operations:

  • Appending a character to the list: O(1)
  • Joining the last 3 elements: O(1) (since it's always at most 3 elements)
  • Comparing strings: O(1) (comparing fixed-length string "abc")
  • Slicing/removing the last 3 elements: O(1) (removing from the end of a list)

Since all operations inside the loop are constant time and we iterate through n characters, the overall time complexity is O(n).

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

The algorithm uses a list t to store characters. In the worst case, when no valid "abc" substrings can be removed (e.g., input like "aabbcc"), all n characters would be stored in the list. Therefore, the space complexity is O(n).

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

Common Pitfalls

1. Inefficient String Concatenation in the Loop

The current solution uses ''.join(stack[-3:]) inside the loop to check if the last three characters form "abc". This creates a new string object every time we check, which adds unnecessary overhead.

Problem:

if ''.join(stack[-3:]) == 'abc':  # Creates a new string each iteration
    stack[-3:] = []

Better Approach:

# Check individual characters directly without string creation
if len(stack) >= 3 and stack[-3] == 'a' and stack[-2] == 'b' and stack[-1] == 'c':
    stack.pop()
    stack.pop()
    stack.pop()

2. Slice Assignment vs Pop Operations

Using stack[-3:] = [] modifies the list in place but may not be as clear or efficient as explicit pop operations.

Current:

stack[-3:] = []  # Removes last 3 elements

Clearer Alternative:

for _ in range(3):
    stack.pop()

3. Missing Early Termination Opportunity

The algorithm could potentially terminate early if the stack grows beyond a certain threshold, as it would be impossible to reduce it to empty.

Enhanced Solution with Optimizations:

class Solution:
    def isValid(self, s: str) -> bool:
        # Early return for invalid lengths
        if len(s) % 3 != 0:
            return False
      
        stack = []
      
        for char in s:
            stack.append(char)
          
            # Direct character comparison without string creation
            if (len(stack) >= 3 and 
                stack[-3] == 'a' and 
                stack[-2] == 'b' and 
                stack[-1] == 'c'):
                # Remove the last 3 characters
                stack.pop()
                stack.pop()
                stack.pop()
      
        return len(stack) == 0

4. Alternative Pitfall: Using String Replacement

Some might attempt to solve this by repeatedly replacing "abc" with an empty string:

Incorrect/Inefficient Approach:

while "abc" in s:
    s = s.replace("abc", "")
return s == ""

Why it's problematic:

  • Each replace() creates a new string (strings are immutable)
  • Time complexity becomes O(n²) in worst case
  • Multiple passes through the string

The stack-based approach is superior as it processes the string in a single pass with O(n) time complexity.

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

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More