1003. Check If Word Is Valid After Substitutions
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 int
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.
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 hasa
,b
,c
which again forms"abc"
, so we remove them - The stack is empty, confirming the string is valid
This approach works because:
- Each valid string must have a length that's a multiple of 3 (since we only add 3 characters at a time)
- At any point during the removal process, finding an
"abc"
pattern means we've identified one insertion operation - 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 settingt[-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 EvaluatorExample 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:
-
Process 'a' (index 0):
- Push 'a' onto stack:
t = ['a']
- Check last 3 elements: only 1 element, no action
- Push 'a' onto stack:
-
Process 'a' (index 1):
- Push 'a' onto stack:
t = ['a', 'a']
- Check last 3 elements: only 2 elements, no action
- Push 'a' onto stack:
-
Process 'b' (index 2):
- Push 'b' onto stack:
t = ['a', 'a', 'b']
- Check last 3 elements:
'aab' ≠ 'abc'
, no removal
- Push 'b' onto stack:
-
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']
- Push 'c' onto stack:
-
Process 'b' (index 4):
- Push 'b' onto stack:
t = ['a', 'b']
- Check last 3 elements: only 2 elements, no action
- Push 'b' onto stack:
-
Process 'c' (index 5):
- Push 'c' onto stack:
t = ['a', 'b', 'c']
- Check last 3 elements:
'abc' == 'abc'
✓ - Remove last 3 elements:
t = []
- Push 'c' onto stack:
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.
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
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!