AlgoMaster Logo

Remove Invalid Parentheses

hardFrequencyUpdated September 21, 2026

Understanding the Problem

We have a string with letters and parentheses, and some of those parentheses are invalid. The task is to find all the ways to remove the minimum number of parentheses so that every remaining parenthesis is properly matched, and return all distinct valid results.

Two requirements make this harder than producing a single valid string. We need every valid result, not just one, and the removal count must be the minimum. If removing 2 parentheses can make the string valid, removing 3 is not allowed even if that also produces a valid string.

A string is valid when scanning left to right keeps a running balance (plus one for (, minus one for )) that never drops below zero and ends at exactly zero. To make a string valid with the fewest removals, we first determine how many parentheses are unmatched: every ) that appears with no unclosed ( before it, and every ( left open at the end. The number of unmatched parentheses is exactly the minimum number of removals.

Key Constraints

  • 1 <= s.length <= 25. The string is short, so exponential approaches that explore subsets of removals are feasible.
  • At most 20 parentheses. Each parenthesis is either kept or removed, so the search space is bounded by 2^20, about a million states. That makes brute-force search practical while still rewarding pruning.

Approach 1: BFS (Level-by-Level Removal)

Intuition

Start with the original string. If it is already valid, return it. Otherwise, generate every string that results from removing one parenthesis, and check whether any of those are valid. If some are, collect them and stop. If none are, repeat: from each of those strings, remove one more parenthesis, and check again.

This is breadth-first search over strings. Each level removes one additional parenthesis. The first level that contains valid strings gives the minimum number of removals, because BFS reaches every string with k removals before any string with k+1 removals. Once a valid string appears at some level, no string at a deeper level can be an answer, so the search stops.

One string can be produced by more than one sequence of removals. For input "())", removing the ) at index 1 and removing the ) at index 2 both produce "()". A visited set tracks which strings have already been generated so each is processed once.

Algorithm

  1. Add the original string to a queue and a visited set.
  2. While the queue is not empty, process all strings at the current level.
  3. For each string, check if it's valid. If valid, add it to the result list.
  4. If we found any valid strings at this level, return the result (minimum removals guaranteed).
  5. Otherwise, for each string, generate all possible strings by removing one parenthesis at each position. Add unseen strings to the queue and visited set.

Example Walkthrough

1Level 0: "()())()" is not valid. The ')' at index 4 has no matching '('
0
(
1
)
2
(
3
)
4
)
unmatched
5
(
6
)
1/7

Code

BFS explores every string at each removal level, including many that can never lead to a valid result. The next approach computes the exact number of ( and ) to remove first, then explores only the choices that respect that budget.

Approach 2: Backtracking with a Removal Budget

Intuition

BFS does not know how many removals are needed, so it regenerates entire levels. We can fix the removal count before searching. Scan the string once with a running balance: every ( is a candidate open, every ) matches the most recent unmatched ( if one exists, otherwise it is unmatched. After the scan, openToRemove is the number of ( left unmatched and closeToRemove is the number of ) that had no ( to match. Those two counts are the exact budget, and any valid answer removes precisely that many of each.

With the budget known, walk the string left to right. At each parenthesis there are two choices: keep it or remove it. Removing a ( spends one unit of openToRemove; removing a ) spends one unit of closeToRemove. Keeping a character also tracks a running balance so prefixes that can never be valid are cut off.

Two checks keep the search inside valid territory. A ) is only kept when the current balance is positive, since keeping it otherwise produces a prefix with more ) than (. A branch is abandoned as soon as a removal counter would go negative, because that would exceed the budget. When the walk reaches the end with balance back to zero and both counters spent, the built string is a valid answer. A set collects results so the same string produced by different removal choices is stored once.

Algorithm

  1. Scan the string once to compute openToRemove (unmatched () and closeToRemove (unmatched )).
  2. Backtrack from index 0 with an empty path and balance 0.
  3. For (: optionally remove it if openToRemove > 0, otherwise keep it and increase balance.
  4. For ): optionally remove it if closeToRemove > 0, and keep it only when balance is positive, decreasing balance.
  5. For a letter: always keep it.
  6. At the end of the string, if balance is 0 and both counters are 0, add the path to the result set.

Example Walkthrough

1Scan once: openToRemove=0, closeToRemove=1 (the ')' at index 4 is unmatched)
0
(
1
)
2
(
3
)
4
)
unmatched
1/10

Code

The budget keeps the search inside valid removal counts, but a set is still needed at the end because different keep/remove choices can build the same string. The next approach removes parentheses position by position and skips over runs of identical characters, so it never generates a duplicate and needs no set at all.

Approach 3: Iterative Removal with Duplicate Skipping

Intuition

The previous approaches rely on a set to drop duplicate strings. Duplicates come from one source: removing any one parenthesis inside a run of identical parentheses produces the same string. Removing the first or the second ) from "))" both leave ")". If, within a run of identical parentheses, removals are restricted to the first one in the run, every distinct result is generated once and no set is needed.

The approach scans for the first position where there are too many ) (the running balance drops below zero). Every excess ) must be fixed by removing one ) at or before that position. For each candidate removal that starts a run of ), recurse on the shortened string. The scan resumes from where it left off rather than restarting, which avoids re-removing earlier parentheses and keeps the work linear per call.

That handles strings with too many ). Too many ( is the mirror image: reverse the string, swap the roles of ( and ), and run the same scan. The second pass catches every unmatched (. Reversing a second time at the end restores the original order. Because each pass only ever removes the closing character of the current orientation, and only at the start of a run, no duplicate is ever produced.

The lastJ parameter remembers where the last removal happened so candidate removals in the next call do not back up before it. Without it, removing position 2 in one branch and position 4 in another could both lead to removing the remaining one and produce the same string twice.

Algorithm

  1. Scan the string from lastI, tracking balance. When balance drops below zero at index i, there is one too many ) up to that point.
  2. For each j from lastJ to i where s[j] is ) and it is the first ) in its run (s[j-1] is not )), remove s[j] and recurse with the scan resuming at i and removals resuming at j.
  3. If the scan finishes without balance going negative, the closing direction is balanced. Reverse the string. If this was the forward pass, run the same procedure on the reversed string with ( and ) swapped. If it was already the reversed pass, the twice-reversed string is a finished answer; add it to the result.

Example Walkthrough

1Forward pass, scan with par=('(',')'). Balance: '(' 1, ')' 0, '(' 1, ')' 0, ')' -1 at index 4
0
(
1
)
2
(
3
)
4
)
balance < 0
1/10

Code