Minimum Remove to Make Valid Parentheses

Medium Stacks

Problem

Given a string s containing parentheses and lowercase English letters, return a string after removing the minimum number of parentheses to make the input string valid. A string is valid if parentheses are balanced and properly closed.

  • 1 ≤ s.length ≤ 10⁵
  • s[i] is either '(', ')', or a lowercase English letter.

Example

Input: s = "a)b(c)d"
Output: "ab(c)d"

Walk through the string from left to right: Index 0: 'a' -> not a parenthesis, keep it. Index 1: ')' -> the stack is empty, so there is no matching '('. Mark this ')' for removal by setting s[1] = "". Index 2: 'b' -> keep it. Index 3: '(' -> push index 3 onto the stack. Index 4: 'c' -> keep it. Index 5: ')' -> the stack is not empty, so we pop 3 from the stack and treat this as a matched pair. Keep this ')'. Index 6: 'd' -> keep it. After this pass, any indices left in the stack would be unmatched '(' positions (none in this example). We then mark those for removal as well. Finally, we join the characters that were not cleared out, which gives "ab(c)d", a valid parentheses string with the minimum removals.

Approach

Straightforward Solution

A brute-force approach might try all subsets of characters to find the minimal removal that yields a valid string, which is exponential and infeasible for large inputs.

Core Observation

A valid parentheses string requires every '(' to be matched with a corresponding ')' in the correct order. Unmatched parentheses must be removed to achieve validity.

Path to Optimal

The key insight is to use a stack to track indices of unmatched '(' characters. When encountering a ')', if the stack is empty, this ')' is unmatched and must be removed. Otherwise, pop from the stack to match the pair. After processing the entire string, any indices left in the stack correspond to unmatched '(' that must be removed. This approach ensures minimal removals in a single pass plus a cleanup pass.

Optimal Approach

Convert the string to a list for in-place edits. Iterate through the list, pushing indices of '(' onto a stack. For each ')', pop from the stack if possible; otherwise, mark the ')' for removal by setting it to an empty string. After the iteration, remove all '(' indices remaining in the stack by setting those positions to empty strings. Finally, join the list into a string and return it.

Time

O(n)

The algorithm makes a single pass through the string to identify invalid parentheses and a second pass to remove unmatched openings, each operation being O(1) per character, resulting in O(n) total time.

Space

O(n)

The stack can hold up to O(n) indices in the worst case (all '(' characters). The string is converted to a list of characters, which is O(n) auxiliary space excluding the input and output.

Pattern Spotlight

Stack (Parentheses Matching)

Use a stack to track indices of unmatched opening parentheses. Unmatched closing parentheses are identified immediately and removed, and any remaining unmatched opening parentheses are removed after processing, ensuring minimal removals for validity.

Solution

1class Solution:
2 def minRemoveToMakeValid(self, s: str) -> str:
3 s = list(s)
4 stack = []
5 for i, c in enumerate(s):
6 if c == '(':
7 stack.append(i)
8 elif c == ')':
9 if stack:
10 stack.pop()
11 else:
12 s[i] = ''
13
14 while stack:
15 s[stack.pop()] = ''
16 return ''.join(s)

Step-by-Step Solution

1

Convert String to List and Initialize Stack for Tracking

3s = list(s)
4stack = []

Objective

To prepare the input for in-place modifications and set up a data structure to track unmatched opening parentheses indices.

Key Insight

Strings in many languages are immutable, so converting to a list allows marking invalid characters by replacing them with empty strings without rebuilding the string repeatedly. The stack stores indices of '(' characters, enabling efficient matching with ')' characters during iteration.

Interview Quick-Check

Core Logic

Converting the string to a list allows O(1) time marking of invalid characters, avoiding costly string concatenations.

State & Boundaries

The stack holds indices of '(' characters that are unmatched so far.

Common Pitfalls & Bugs

Forgetting to convert the string to a mutable structure leads to inefficient or incorrect removal of characters.

2

Iterate Through Characters to Identify Invalid Parentheses

5for i, c in enumerate(s):
6 if c == '(':
7 stack.append(i)
8 elif c == ')':
9 if stack:
10 stack.pop()
11 else:
12 s[i] = ''

Objective

To scan the string and use the stack to match parentheses, marking unmatched closing parentheses for removal.

Key Insight

When encountering '(', push its index onto the stack to await a matching ')'. When encountering ')', if the stack is not empty, pop to match; otherwise, mark the ')' as invalid by replacing it with an empty string. This ensures that unmatched ')' are removed immediately, and unmatched '(' remain on the stack for later removal.

Interview Quick-Check

Core Logic

The stack-based matching ensures that every ')' is matched with the most recent unmatched '(', preserving correct nesting and order.

State & Boundaries

If the stack is empty when encountering ')', it is unmatched and must be removed.

Common Pitfalls & Bugs

Failing to check if the stack is empty before popping leads to errors or incorrect matches.

3

Remove Remaining Unmatched Opening Parentheses and Rebuild String

14while stack:
15 s[stack.pop()] = ''
16return ''.join(s)

Objective

To remove all unmatched '(' characters left on the stack and return the valid string.

Key Insight

After the first pass, any indices left in the stack correspond to '(' without matching ')'. Marking these positions as empty strings removes them. Joining the list back into a string produces the final valid string with minimal removals.

Interview Quick-Check

Core Logic

Removing unmatched '(' after the iteration ensures minimal removals and correctness, as these unmatched openings cannot be paired.

State & Boundaries

The while loop continues until all unmatched '(' indices are removed.

Complexity

Joining the list into a string is O(n), which is efficient given the problem constraints.

Line Analysis

This solution has 4 Critical lines interviewers watch for.

Line 7 Critical
stack.append(i)

Push the index of '(' onto the stack.

This line is critical because it records the position of an unmatched '(' to enable correct pairing or removal later, forming the backbone of the stack-based matching algorithm.

Line 9 Critical
if stack:

Check if there is an unmatched '(' available to match with the current ')'.

This check is crucial to avoid invalid matches and to identify unmatched ')' that must be removed, preserving correctness.

Line 12 Critical
s[i] = ''

Mark the unmatched ')' for removal by replacing it with an empty string.

This line's placement is critical to immediately mark unmatched ')' for removal, ensuring minimal removals and preserving the original string structure for subsequent processing.

Line 15 Critical
s[stack.pop()] = ''

Mark the unmatched '(' at the popped index for removal by replacing it with an empty string.

Removing unmatched '(' after the full scan is essential because these openings have no matching closing parentheses, ensuring the final string is valid with minimal removals.

Line 4 Core
stack = []

Initialize an empty stack to store indices of unmatched opening parentheses.

The stack tracks positions of '(' characters that have not yet been matched with a corresponding ')', enabling efficient matching during iteration.

Line 10 Core
stack.pop()

Pop the index of the matched '(' from the stack.

Removing the matched '(' index signifies a successful pairing, maintaining the stack's invariant of unmatched openings.

Line 11 Core
else:

Handle the case where the current ')' is unmatched.

If no matching '(' exists, this ')' must be removed to maintain validity.

Line 14 Core
while stack:

Begin removing unmatched '(' characters remaining in the stack.

After processing the entire string, any '(' indices left in the stack are unmatched and must be removed to achieve validity.

Line 3 Core
s = list(s)

Convert the input string into a list of characters for in-place modification.

Strings are immutable, so converting to a list allows marking invalid parentheses by replacing them with empty strings efficiently without rebuilding the string repeatedly.

Line 6 Core
if c == '(':

Check if the current character is an opening parenthesis '('.

Identifying '(' characters is essential to push their indices onto the stack for future matching.

Line 8 Core
elif c == ')':

Check if the current character is a closing parenthesis ')'.

Identifying ')' characters triggers the matching logic with the stack to determine validity.

Line 16 Core
return ''.join(s)

Join the list of characters into a string and return the valid string.

Joining the list reconstructs the string without the removed parentheses, producing the final valid output.

Line 5 Standard
for i, c in enumerate(s):

Begin iterating over the list with index and character.

Enumerating both index and character is necessary to track positions for potential removal and to identify parentheses.

Test Your Understanding

Why is it necessary to remove unmatched opening parentheses after processing the entire string rather than during the initial iteration?

Because unmatched opening parentheses can only be identified after processing all characters; they remain on the stack if no matching closing parenthesis is found later, so they must be removed after the full scan to ensure minimal removals.

Related Problems

Stacks pattern

Don't just read it. Drill it.

Reconstruct Minimum Remove to Make Valid Parentheses from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Minimum Remove to Make Valid Parentheses drill

or drill a free problem