Remove the minimum number of invalid parentheses in order to make the input string valid. Return all possible results.
Note: The input string may contain letters other than the parentheses (
and )
.
Example 1:
Input: "()())()" Output: ["()()()", "(())()"]
Example 2:
Input: "(a)())()" Output: ["(a)()()", "(a())()"]
Example 3:
Input: ")(" Output: [""]
Credits:
Special thanks to @hpplayer for adding this problem and creating all test cases.
Subscribe to see which companies asked this question
解法一:
class Solution { public: vector<string> removeInvalidParentheses(string s) { vector<string> res; unordered_set<string> visited{{s}}; queue<string> q{{s}}; bool found = false; while (!q.empty()) { string t = q.front(); q.pop(); if (isValid(t)) { res.push_back(t); found = true; } if (found) continue; for (int i = 0; i < t.size(); ++i) { if (t[i] != '(' && t[i] != ')') continue; string str = t.substr(0, i) + t.substr(i + 1); if (!visited.count(str)) { q.push(str); visited.insert(str); } } } return res; } bool isValid(string t) { int cnt = 0; for (int i = 0; i < t.size(); ++i) { if (t[i] == '(') ++cnt; else if (t[i] == ')' && --cnt < 0) return false; } return cnt == 0; } };
下面來看一種遞歸解法,這種解法首先統計了多余的半括號的數量,用 cnt1 表示多余的左括號,cnt2 表示多余的右括號,因為給定字符串左右括號要么一樣多,要么左括號多,要么右括號多,也可能左右括號都多,比如 ")("。所以 cnt1 和 cnt2 要么都為0,要么都大於0,要么一個為0,另一個大於0。好,下面進入遞歸函數,首先判斷,如果當 cnt1 和 cnt2 都為0時,說明此時左右括號個數相等了,調用 isValid 子函數來判斷是否正確,正確的話加入結果 res 中並返回即可。否則從 start 開始遍歷,這里的變量 start 表示當前遞歸開始的位置,不需要每次都從頭開始,會有大量重復計算。而且對於多個相同的半括號在一起,只刪除第一個,比如 "())",這里有兩個右括號,不管刪第一個還是刪第二個右括號都會得到 "()",沒有區別,所以只用算一次就行了,通過和上一個字符比較,如果不相同,說明是第一個右括號,如果相同則直接跳過。此時來看如果 cnt1 大於0,說明此時左括號多,而如果當前字符正好是左括號的時候,可以刪掉當前左括號,繼續調用遞歸,此時 cnt1 的值就應該減1,因為已經刪掉了一個左括號。同理,如果 cnt2 大於0,說明此時右括號多,而如果當前字符正好是右括號的時候,可以刪掉當前右括號,繼續調用遞歸,此時 cnt2 的值就應該減1,因為已經刪掉了一個右括號,參見代碼如下:
解法二:
class Solution { public: vector<string> removeInvalidParentheses(string s) { vector<string> res; int cnt1 = 0, cnt2 = 0; for (char c : s) { cnt1 += (c == '('); if (cnt1 == 0) cnt2 += (c == ')'); else cnt1 -= (c == ')'); } helper(s, 0, cnt1, cnt2, res); return res; } void helper(string s, int start, int cnt1, int cnt2, vector<string>& res) { if (cnt1 == 0 && cnt2 == 0) { if (isValid(s)) res.push_back(s); return; } for (int i = start; i < s.size(); ++i) { if (i != start && s[i] == s[i - 1]) continue; if (cnt1 > 0 && s[i] == '(') { helper(s.substr(0, i) + s.substr(i + 1), i, cnt1 - 1, cnt2, res); } if (cnt2 > 0 && s[i] == ')') { helper(s.substr(0, i) + s.substr(i + 1), i, cnt1, cnt2 - 1, res); } } } bool isValid(string t) { int cnt = 0; for (int i = 0; i < t.size(); ++i) { if (t[i] == '(') ++cnt; else if (t[i] == ')' && --cnt < 0) return false; } return cnt == 0; } };
下面這種解法是論壇上的高票解法,思路確實很巧妙。遞歸函數的參數中,last_i 表示當前遍歷到的位置,相當上面解法中的 start,last_j 表示上一個刪除的位置,這樣可以避免重復計算。然后有個括號字符數組,初始化時放入左括號和右括號,博主認為這個字符數組是此解法最精髓的地方,因為其順序可以改變,可以變成反向括號,這個就比較叼了,后面再講它到底有多叼吧。在遞歸函數中,從 last_i 開始遍歷,在找正向括號的時候,用變量 cnt 表示括號數組中的左括號出現的次數,遇到左括號自增1,遇到右括號自減1。當左括號大於等於右括號的時候,直接跳過。這個循環的目的是要刪除多余的右括號,所以當 cnt 小於0的時候,從上一個刪除位置 last_j 開始遍歷,如果當前是右括號,且是第一個右括號(關於這塊可以參見上面解法中的分析),刪除當前右括號,並調用遞歸函數。注意這個 for 循環結束后要直接返回,因為進這個 for 循環的都是右括號多的,刪到最后最多是刪成和左括號一樣多,不需要再去翻轉刪左括號。好,最后來說這個最叼的翻轉,當字符串的左括號個數大於等於右括號的時候,不會進入第二個 for 循環,自然也不會 return。那么由於左括號的個數可能會要大於右括號,所以還要刪除多余的左括號,將字符串反轉一下,比如 "(()",反轉變成 ")((",此時雖然還是要刪除多余的左括號,但是反轉后就沒有合法的括號了,所以變成了找反向括號 ")(",還是可以刪除多余的左括號,然后判斷此時括號數組的狀態,如果是正向括號,說明此時正要刪除左括號,就調用遞歸函數,last_i 和 last_j 均重置為0,括號數組初始化為反向括號。如果此時已經是反向括號了,說明之前的左括號已經刪掉了變成了 ")(",然后又反轉了一下,變回來了 "()",就可以直接加入結果 res 了,參見代碼如下:
解法三:
class Solution { public: vector<string> removeInvalidParentheses(string s) { vector<string> res; helper(s, 0, 0, {'(', ')'}, res); return res; } void helper(string s, int last_i, int last_j, vector<char> p, vector<string>& res) { int cnt = 0; for (int i = last_i; i < s.size(); ++i) { if (s[i] == p[0]) ++cnt; else if (s[i] == p[1]) --cnt; if (cnt >= 0) continue; for (int j = last_j; j <= i; ++j) { if (s[j] == p[1] && (j == last_j || s[j] != s[j - 1])) { helper(s.substr(0, j) + s.substr(j + 1), i, j, p, res); } } return; } string rev = string(s.rbegin(), s.rend()); if (p[0] == '(') helper(rev, 0, 0, {')', '('}, res); else res.push_back(rev); } };
下面這種解法由熱心網友 fvglty 提供,應該算是一種暴力搜索的方法,並沒有太多的技巧在里面,但是思路直接了當,可以作為為面試中最先提出的解法。思路是先將s放到一個 HashSet 中,然后進行該集合 cur 不為空的 while 循環,此時新建另一個集合 next,遍歷之前的集合 cur,若某個字符串是合法的括號,直接加到結果 res 中,並且看若 res 不為空,則直接跳過。跳過的部分實際上是去除括號的操作,由於不知道該去掉哪個半括號,所以只要遇到半括號就都去掉,然后加入另一個集合 next 中,這里實際上保存的是下一層的候選者。當前的 cur 遍歷完成后,若 res 不為空,則直接返回,因為這是當前層的合法括號,一定是移除數最少的。若 res 為空,則將 next 賦值給 cur,繼續循環,參見代碼如下:
解法四:
class Solution { public: vector<string> removeInvalidParentheses(string s) { vector<string> res; unordered_set<string> cur{{s}}; while (!cur.empty()) { unordered_set<string> next; for (auto &a : cur) { if (isValid(a)) res.push_back(a); if (!res.empty()) continue; for (int i = 0; i < a.size(); ++i) { if (a[i] != '(' && a[i] != ')') continue; next.insert(a.substr(0, i) + a.substr(i + 1)); } } if (!res.empty()) return res; cur = next; } return res; } bool isValid(string t) { int cnt = 0; for (int i = 0; i < t.size(); ++i) { if (t[i] == '(') ++cnt; else if (t[i] == ')' && --cnt < 0) return false; } return cnt == 0; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/301
類似題目:
Different Ways to Add Parentheses
參考資料:
https://leetcode.com/problems/remove-invalid-parentheses/
https://leetcode.com/problems/remove-invalid-parentheses/discuss/75032/share-my-java-bfs-solution