Loading problems...
TL;DR: Calculate the exact number of misplaced left and right parentheses, then use a Depth-First Search (DFS) backtracking strategy to remove specific characters until the string is valid and minimal removals are achieved.
The "Remove Invalid Parentheses" problem asks us to take a string containing parentheses and letters and remove the minimum number of parentheses required to make the string valid. A valid string is defined by standard mathematical rules: every opening parenthesis must have a corresponding closing parenthesis, and they must be properly nested. The output must include all unique valid strings that can be formed with the minimum number of removals.
This is a popular interview question because it tests the ability to manage complex state during recursion and optimize search spaces using pruning techniques.
A naive brute force approach would involve generating every possible subsequence of the input string and checking two conditions:
This can be implemented by iterating through all possible lengths (from s.length() down to 0) and generating all combinations of that length.
plaintext
function bruteForce(s):
for length from s.length down to 0:
found_valid = false
results = []
for each subsequence of given length:
if isValid(subsequence):
results.add(subsequence)
found_valid = true
if found_valid:
return resultsWhy this fails: The time complexity of generating all subsequences is . For a string of length 25, this involves millions of operations. Additionally, checking validity takes for each subsequence. This approach will result in a Time Limit Exceeded (TLE) error because it blindly explores invalid branches and does not utilize the information about which parentheses are causing the issue.
The key to optimizing the solution lies in identifying the "target" state before starting the recursion. Instead of blindly removing characters, we first scan the string to determine exactly how many open parentheses ( and closed parentheses ) are misplaced.
(, increment a counter for expected closing parentheses.), decrement the counter. If the counter is already 0, this ) is invalid and must be removed.( that must be removed.rem_open (count of ( to remove) and rem_close (count of ) to remove).balance variable. balance increments on ( and decrements on ). If balance ever becomes negative, the current path is invalid (we closed more than we opened).Visual Description: Imagine the recursion tree starting at the first index of the string. At each node (character), the tree branches. One branch represents "Keeping" the character (appending it to our potential solution). The other branch represents "Removing" the character (skipping it).
rem_open or rem_close > 0).![]()
Let's trace the logic for input s = "()())()".
).left_rem = 0, right_rem = 1.()()): The algorithm keeps these as they are valid. balance fluctuates but stays .)):
) in the sequence ...)).right_rem > 0, we can skip this character. right_rem becomes 0. We proceed to build the rest. Result: (())().(): Processed normally.)): Processed normally.left_rem, right_rem, and balance are all 0. Valid strings are added to a Set to handle uniqueness automatically.The algorithm is correct because it exhaustively explores all combinations of removals that match the minimum count calculated in the first step.
left_rem and right_rem limits, we never remove more parentheses than absolutely necessary.balance >= 0 check during the "Keep" phase ensures that we never close a parenthesis that hasn't been opened. The final balance == 0 check ensures all opened parentheses are closed.cpp#include <iostream> #include <vector> #include <string> #include <unordered_set> using namespace std; class Solution { public: vector<string> removeInvalidParentheses(string s) { int left_rem = 0, right_rem = 0; // Phase 1: Calculate minimum removals needed for (char c : s) { if (c == '(') { left_rem++; } else if (c == ')') { if (left_rem > 0) { left_rem--; } else { right_rem++; } } } unordered_set<string> result; string current_expr = ""; // Phase 2: Backtracking backtrack(s, 0, left_rem, right_rem, 0, current_expr, result); return vector<string>(result.begin(), result.end()); } private: void backtrack(const string& s, int index, int l_rem, int r_rem, int balance, string& current, unordered_set<string>& result) { // Base Case: End of string if (index == s.length()) { if (l_rem == 0 && r_rem == 0 && balance == 0) { result.insert(current); } return; } char c = s[index]; // Pruning: If balance is negative, this path is invalid (cannot recover) if (balance < 0) return; // Choice 1: Remove current character (only if it's a paren and we have quota) if (c == '(' && l_rem > 0) { backtrack(s, index + 1, l_rem - 1, r_rem, balance, current, result); } else if (c == ')' && r_rem > 0) { backtrack(s, index + 1, l_rem, r_rem - 1, balance, current, result); } // Choice 2: Keep current character current.push_back(c); if (c != '(' && c != ')') { // It's a letter, balance doesn't change backtrack(s, index + 1, l_rem, r_rem, balance, current, result); } else if (c == '(') { backtrack(s, index + 1, l_rem, r_rem, balance + 1, current, result); } else if (c == ')') { backtrack(s, index + 1, l_rem, r_rem, balance - 1, current, result); } // Backtrack current.pop_back(); } };
javaimport java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; class Solution { public List<String> removeInvalidParentheses(String s) { int leftRem = 0; int rightRem = 0; // Calculate misplaced parentheses for (char c : s.toCharArray()) { if (c == '(') { leftRem++; } else if (c == ')') { if (leftRem > 0) { leftRem--; } else { rightRem++; } } } Set<String> result = new HashSet<>(); StringBuilder sb = new StringBuilder(); backtrack(s, 0, leftRem, rightRem, 0, sb, result); return new ArrayList<>(result); } private void backtrack(String s, int index, int lRem, int rRem, int balance, StringBuilder current, Set<String> result) { if (index == s.length()) { if (lRem == 0 && rRem == 0 && balance == 0) { result.add(current.toString()); } return; } char c = s.charAt(index); // Optimization: Stop if balance goes negative (impossible to recover) if (balance < 0) return; // Logic to remove character // We only remove if we have a quota (lRem or rRem > 0) // To avoid duplicates, we could add logic here, or just rely on the Set if (c == '(' && lRem > 0) { backtrack(s, index + 1, lRem - 1, rRem, balance, current, result); } else if (c == ')' && rRem > 0) { backtrack(s, index + 1, lRem, rRem - 1, balance, current, result); } // Logic to keep character current.append(c); if (c != '(' && c != ')') { backtrack(s, index + 1, lRem, rRem, balance, current, result); } else if (c == '(') { backtrack(s, index + 1, lRem, rRem, balance + 1, current, result); } else if (c == ')') { backtrack(s, index + 1, lRem, rRem, balance - 1, current, result); } // Undo change (Backtrack) current.deleteCharAt(current.length() - 1); } }
pythonclass Solution: def removeInvalidParentheses(self, s: str) -> list[str]: # Phase 1: Count invalid parentheses left_rem, right_rem = 0, 0 for char in s: if char == '(': left_rem += 1 elif char == ')': if left_rem > 0: left_rem -= 1 else: right_rem += 1 result = set() # Phase 2: DFS Backtracking def backtrack(index, l_rem, r_rem, balance, current_path): # Base Case: End of string if index == len(s): if l_rem == 0 and r_rem == 0 and balance == 0: result.add("".join(current_path)) return # Pruning: Balance cannot be negative if balance < 0: return char = s[index] # Decision 1: Remove the character # Only possible if we have removals left for that type if char == '(' and l_rem > 0: backtrack(index + 1, l_rem - 1, r_rem, balance, current_path) elif char == ')' and r_rem > 0: backtrack(index + 1, l_rem, r_rem - 1, balance, current_path) # Decision 2: Keep the character current_path.append(char) if char == '(': backtrack(index + 1, l_rem, r_rem, balance + 1, current_path) elif char == ')': backtrack(index + 1, l_rem, r_rem, balance - 1, current_path) else: # Letters don't affect balance backtrack(index + 1, l_rem, r_rem, balance, current_path) # Backtrack step current_path.pop() backtrack(0, left_rem, right_rem, 0, []) return list(result)
Time Complexity:
In the worst case (e.g., ((((((), we might have many branches. However, since we limit the depth of the "remove" branches to left_rem + right_rem, the effective complexity is closer to , where is the number of removals. Given , this is well within limits.
Space Complexity: The recursion stack depth goes up to . We also store the valid strings, but since is small, the number of unique valid strings is relatively small.
The pattern Backtracking - Parentheses Generation is highly transferable. Understanding how to manage balance and recursion depth applies directly to:
left_count and right_count mirrors the left_rem and right_rem logic here.Why does the naive solution result in TLE?
Why am I getting duplicate strings in the output?
(() properly.( results in the same string as removing the second (.Set (as shown in the reference solutions) or by adding a condition to skip consecutive duplicates during the "remove" decision.Why is my solution returning invalid strings like )(?
( and ) is not enough; they must be ordered. )( has counts 1 and 1, but is invalid.balance (never allowing it to drop below 0) to ensure structural validity.