1249. Minimum Remove to Make Valid Parentheses
Problem Description
You are given a string s
that contains opening parentheses '('
, closing parentheses ')'
, and lowercase English letters.
Your task is to remove the minimum number of parentheses (either '('
or ')'
) from any positions in the string to make the remaining parentheses valid. Return any valid string after the removal.
A valid parentheses string follows these rules:
- It can be an empty string or contain only lowercase letters
- It can be formed by concatenating two valid strings
A
andB
(written asAB
) - It can be written as
(A)
whereA
is a valid string (meaning every opening parenthesis has a matching closing parenthesis in the correct order)
For example:
"lee(t(c)o)de"
is valid (all parentheses are balanced)"a)b(c)d"
is invalid (the first)
has no matching(
)"))((
is invalid (parentheses are not in the correct order)
The solution uses a two-pass approach:
- First pass (left to right): Remove excess closing parentheses
)
that don't have matching opening parentheses before them - Second pass (right to left): Remove excess opening parentheses
(
that don't have matching closing parentheses after them
The algorithm maintains a counter x
to track the balance of parentheses. In the first pass, it skips any )
when there are no unmatched (
available. In the second pass, it processes the result from the first pass in reverse, skipping any (
when there are no unmatched )
available. This ensures the minimum number of parentheses are removed while maintaining validity.
Intuition
The key insight is that valid parentheses must satisfy two conditions: every '('
must have a matching ')'
that comes after it, and every ')'
must have a matching '('
that comes before it.
When we scan from left to right, we can easily identify invalid ')'
characters - these are closing parentheses that appear when we haven't seen enough opening parentheses yet. We can keep a counter that increments for '('
and decrements for ')'
. If we encounter a ')'
when the counter is 0, we know this ')'
is invalid and must be removed.
However, this single pass doesn't solve the problem completely. We might have excess '('
characters that never get matched. For example, in "((("
, all opening parentheses pass the left-to-right scan, but they're still invalid because they lack matching closing parentheses.
To handle excess '('
characters, we need a second pass from right to left. This time, we treat the problem in reverse - we're looking for '('
characters that don't have matching ')'
characters to their right. By processing the string backwards, we can identify and remove these unmatched opening parentheses using the same counter logic.
The brilliance of this two-pass approach is that:
- The first pass guarantees no
')'
appears without a preceding'('
- The second pass guarantees no
'('
remains without a following')'
- Together, they ensure we remove the minimum number of parentheses because we only remove characters that definitely cannot be matched
This greedy approach works because once we remove invalid ')'
in the first pass, those removals don't affect the validity of the remaining '('
characters - we're simply ensuring balance from both directions.
Learn more about Stack patterns.
Solution Approach
The solution implements a two-pass algorithm to remove invalid parentheses while preserving all valid ones and lowercase letters.
First Pass (Left to Right):
We initialize an empty stack stk
and a counter x = 0
to track unmatched opening parentheses. As we iterate through each character c
in the string:
- If
c
is')'
andx == 0
, we skip this character (don't add it to the stack) because there's no unmatched'('
to pair with - If
c
is'('
, we incrementx
and add it to the stack - If
c
is')'
andx > 0
, we decrementx
and add it to the stack - If
c
is a lowercase letter, we simply add it to the stack
After this pass, stk
contains all characters except the invalid ')'
that appeared without matching '('
.
Second Pass (Right to Left):
We reset x = 0
and create a new list ans
. We process the stack from the first pass in reverse order (stk[::-1]
):
- If
c
is'('
andx == 0
, we skip this character because there's no unmatched')'
to its right - If
c
is')'
, we incrementx
and add it toans
- If
c
is'('
andx > 0
, we decrementx
and add it toans
- If
c
is a lowercase letter, we add it toans
Final Result:
Since we processed the characters in reverse during the second pass, we need to reverse ans
again to get the correct order: ''.join(ans[::-1])
.
The algorithm ensures correctness because:
- The first pass removes all
')'
that cannot be matched with a preceding'('
- The second pass removes all
'('
that cannot be matched with a following')'
- Both passes preserve all lowercase letters
- The counter mechanism ensures we only remove the minimum necessary parentheses
Time and Space Complexity:
- Time Complexity:
O(n)
wheren
is the length of the string, as we make exactly two passes through the string - Space Complexity:
O(n)
for storing the intermediate stack and final result
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 = "a)b(c)d"
.
First Pass (Left to Right):
- Start with
stk = []
andx = 0
(tracks unmatched(
) - Process each character:
'a'
: lowercase letter → add to stk →stk = ['a']
')'
: closing parenthesis withx = 0
(no unmatched(
) → skip it'b'
: lowercase letter → add to stk →stk = ['a', 'b']
'('
: opening parenthesis → incrementx = 1
, add to stk →stk = ['a', 'b', '(']
'c'
: lowercase letter → add to stk →stk = ['a', 'b', '(', 'c']
')'
: closing parenthesis withx = 1
→ decrementx = 0
, add to stk →stk = ['a', 'b', '(', 'c', ')']
'd'
: lowercase letter → add to stk →stk = ['a', 'b', '(', 'c', ')', 'd']
After first pass: stk = ['a', 'b', '(', 'c', ')', 'd']
(removed the invalid first ')'
)
Second Pass (Right to Left):
- Start with
ans = []
andx = 0
(tracks unmatched)
) - Process reversed stk:
['d', ')', 'c', '(', 'b', 'a']
'd'
: lowercase letter → add to ans →ans = ['d']
')'
: closing parenthesis → incrementx = 1
, add to ans →ans = ['d', ')']
'c'
: lowercase letter → add to ans →ans = ['d', ')', 'c']
'('
: opening parenthesis withx = 1
→ decrementx = 0
, add to ans →ans = ['d', ')', 'c', '(']
'b'
: lowercase letter → add to ans →ans = ['d', ')', 'c', '(', 'b']
'a'
: lowercase letter → add to ans →ans = ['d', ')', 'c', '(', 'b', 'a']
Final Result:
Reverse ans
to get the correct order: ''.join(['a', 'b', '(', 'c', ')', 'd'])
= "ab(c)d"
The algorithm successfully removed the minimum number of parentheses (just one ')'
) to make the string valid.
Solution Implementation
1class Solution:
2 def minRemoveToMakeValid(self, s: str) -> str:
3 """
4 Remove minimum number of parentheses to make a valid parentheses string.
5
6 Two-pass approach:
7 1. First pass (left to right): Remove invalid closing parentheses
8 2. Second pass (right to left): Remove invalid opening parentheses
9
10 Args:
11 s: Input string containing letters and parentheses
12
13 Returns:
14 Valid string with minimum parentheses removed
15 """
16 # First pass: Remove invalid closing parentheses ')'
17 stack = []
18 open_count = 0
19
20 for char in s:
21 # Skip closing parenthesis if no matching opening parenthesis
22 if char == ')' and open_count == 0:
23 continue
24
25 # Track opening parentheses count
26 if char == '(':
27 open_count += 1
28 elif char == ')':
29 open_count -= 1
30
31 stack.append(char)
32
33 # Second pass: Remove invalid opening parentheses '(' from the end
34 result = []
35 close_count = 0
36
37 # Process the stack in reverse order
38 for char in reversed(stack):
39 # Skip opening parenthesis if no matching closing parenthesis
40 if char == '(' and close_count == 0:
41 continue
42
43 # Track closing parentheses count (in reverse)
44 if char == ')':
45 close_count += 1
46 elif char == '(':
47 close_count -= 1
48
49 result.append(char)
50
51 # Reverse the result to restore original order
52 return ''.join(reversed(result))
53
1class Solution {
2 public String minRemoveToMakeValid(String s) {
3 // Stack to store characters during first pass (left to right)
4 Deque<Character> stack = new ArrayDeque<>();
5 // Counter for unmatched opening parentheses
6 int unmatchedOpenCount = 0;
7
8 // First pass: traverse from left to right to remove invalid closing parentheses
9 for (int i = 0; i < s.length(); i++) {
10 char currentChar = s.charAt(i);
11
12 // Skip closing parenthesis if there's no matching opening parenthesis
13 if (currentChar == ')' && unmatchedOpenCount == 0) {
14 continue;
15 }
16
17 // Update counter based on parenthesis type
18 if (currentChar == '(') {
19 unmatchedOpenCount++;
20 } else if (currentChar == ')') {
21 unmatchedOpenCount--;
22 }
23
24 // Add valid character to stack
25 stack.push(currentChar);
26 }
27
28 // StringBuilder to construct the final result
29 StringBuilder result = new StringBuilder();
30 // Counter for unmatched closing parentheses during second pass
31 int unmatchedCloseCount = 0;
32
33 // Second pass: traverse from right to left (via stack) to remove invalid opening parentheses
34 while (!stack.isEmpty()) {
35 char currentChar = stack.pop();
36
37 // Skip opening parenthesis if there's no matching closing parenthesis
38 if (currentChar == '(' && unmatchedCloseCount == 0) {
39 continue;
40 }
41
42 // Update counter based on parenthesis type
43 if (currentChar == ')') {
44 unmatchedCloseCount++;
45 } else if (currentChar == '(') {
46 unmatchedCloseCount--;
47 }
48
49 // Append valid character to result
50 result.append(currentChar);
51 }
52
53 // Reverse the result since we built it backwards (from stack popping)
54 return result.reverse().toString();
55 }
56}
57
1class Solution {
2public:
3 string minRemoveToMakeValid(string s) {
4 // First pass: remove invalid closing parentheses
5 // Build a string while tracking open parentheses count
6 string afterFirstPass;
7 int openCount = 0;
8
9 for (char& ch : s) {
10 // Skip closing parenthesis if there's no matching opening
11 if (ch == ')' && openCount == 0) {
12 continue;
13 }
14
15 // Update open parentheses counter
16 if (ch == '(') {
17 ++openCount;
18 } else if (ch == ')') {
19 --openCount;
20 }
21
22 // Add valid character to result
23 afterFirstPass.push_back(ch);
24 }
25
26 // Second pass: remove unmatched opening parentheses from right to left
27 // Process the string backwards to remove excess opening parentheses
28 string result;
29 int closeCount = 0;
30
31 // Iterate from the end of the string
32 while (!afterFirstPass.empty()) {
33 char ch = afterFirstPass.back();
34 afterFirstPass.pop_back();
35
36 // Skip opening parenthesis if there's no matching closing
37 if (ch == '(' && closeCount == 0) {
38 continue;
39 }
40
41 // Update close parentheses counter
42 if (ch == ')') {
43 ++closeCount;
44 } else if (ch == '(') {
45 --closeCount;
46 }
47
48 // Add valid character to result
49 result.push_back(ch);
50 }
51
52 // Reverse to get the correct order since we built it backwards
53 reverse(result.begin(), result.end());
54
55 return result;
56 }
57};
58
1/**
2 * Removes the minimum number of parentheses to make a valid parentheses string
3 * @param s - Input string containing letters and parentheses
4 * @returns Valid string with minimum parentheses removed
5 */
6function minRemoveToMakeValid(s: string): string {
7 // First pass: count valid pairs of parentheses
8 let openCount: number = 0; // Track open parentheses
9 let validPairs: number = 0; // Track valid closing parentheses that can be paired
10
11 for (const char of s) {
12 if (char === '(') {
13 openCount++;
14 } else if (char === ')') {
15 // Only count this closing parenthesis if there's a matching open
16 if (validPairs < openCount) {
17 validPairs++;
18 }
19 }
20 }
21
22 // Second pass: build result string with valid parentheses only
23 let usedOpenCount: number = 0; // Track open parentheses added to result
24 let result: string = '';
25
26 for (const char of s) {
27 if (char === '(') {
28 // Only add open parenthesis if it can be paired with a closing one
29 if (usedOpenCount < validPairs) {
30 usedOpenCount++;
31 result += char;
32 }
33 } else if (char === ')') {
34 // Only add closing parenthesis if there's a matching open in result
35 if (usedOpenCount > 0 && validPairs > 0) {
36 validPairs--;
37 usedOpenCount--;
38 result += char;
39 }
40 } else {
41 // Always keep non-parenthesis characters
42 result += char;
43 }
44 }
45
46 return result;
47}
48
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input string s
.
The algorithm consists of two main passes through the string:
- First pass: Iterates through each character in
s
once, performing constant-time operations (checking character type, incrementing/decrementing counter, appending to list). This takesO(n)
time. - Second pass: Iterates through the reversed
stk
list, which has at mostn
elements. Each operation within this loop (checking character type, incrementing/decrementing counter, appending to list) takes constant time. This also takesO(n)
time. - Final operation:
''.join(ans[::-1])
requires reversing theans
list (O(n)
) and joining the characters (O(n)
).
Total time complexity: O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
where n
is the length of the input string s
.
The algorithm uses the following additional space:
stk
list: In the worst case (when no characters are removed in the first pass), this stores alln
characters from the input string, requiringO(n)
space.ans
list: In the worst case, this stores all valid characters fromstk
, which could be up ton
characters, requiringO(n)
space.x
counter variable: Uses constant spaceO(1)
.
Total space complexity: O(n) + O(n) + O(1) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Remove Parentheses in a Single Pass
A common mistake is trying to identify and remove invalid parentheses in just one pass through the string. This approach fails because you cannot determine if an opening parenthesis '('
is valid until you've seen the entire string after it.
Why it fails:
# Incorrect single-pass attempt
def minRemoveToMakeValid_wrong(s: str) -> str:
result = []
balance = 0
for char in s:
if char == '(':
balance += 1
result.append(char)
elif char == ')':
if balance > 0:
balance -= 1
result.append(char)
else:
result.append(char)
return ''.join(result)
# This fails for input like "(((" or "a(b(c)d"
# It would return "(((" or "a(b(c)d" instead of "" or "ab(c)d"
Solution: Use the two-pass approach where the first pass handles excess ')'
and the second pass (in reverse) handles excess '('
.
2. Forgetting to Reverse After the Second Pass
Since the second pass processes characters in reverse order, the result must be reversed again to restore the original order. Forgetting this final reversal is a frequent error.
Why it fails:
# Missing final reversal
def minRemoveToMakeValid_wrong(s: str) -> str:
# First pass code...
# Second pass
result = []
close_count = 0
for char in reversed(stack):
# Process characters...
result.append(char)
return ''.join(result) # Wrong! This gives reversed output
# For "lee(t(c)o)de" you'd get "ed)o)c(t(eel"
Solution: Always reverse the result after the second pass: return ''.join(reversed(result))
3. Incorrectly Managing the Counter in the Second Pass
In the second pass, you're processing from right to left, so the logic is inverted - you're counting closing parentheses ')'
and matching them with opening parentheses '('
.
Why it fails:
# Wrong counter logic in second pass
for char in reversed(stack):
if char == '(' and open_count == 0: # Wrong! Should check close_count
continue
if char == '(':
open_count += 1 # Wrong! Should decrement close_count
elif char == ')':
open_count -= 1 # Wrong! Should increment close_count
Solution: In the second pass, track close_count
: increment for ')'
and decrement for '('
when valid.
4. Not Handling Edge Cases Properly
Failing to consider edge cases can lead to unexpected behavior:
- Empty string
""
- String with only letters
"abc"
- String with only parentheses
"((("
- Completely invalid parentheses
")))((("
Solution: The two-pass approach naturally handles all these cases without special treatment, but it's important to verify your implementation works for them.
Which of these pictures shows the visit order of a depth-first search?
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!