[LeetCode] 301. Remove Invalid Parentheses 移除非法括號


 

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

 
這道題讓移除最少的括號使得給定字符串為一個合法的含有括號的字符串,我們從小數學里就有括號,所以應該對合法的含有括號的字符串並不陌生,字符串中的左右括號數應該相同,而且每個右括號左邊一定有其對應的左括號,而且題目中給的例子也說明了去除方法不唯一,需要找出所有合法的取法。參考了網上大神的解法,這道題首先可以用 BFS 來解,我把給定字符串排入隊中,然后取出檢測其是否合法,若合法直接返回,不合法的話,對其進行遍歷,對於遇到的左右括號的字符,去掉括號字符生成一個新的字符串,如果這個字符串之前沒有遇到過,將其排入隊中,用 HashSet 記錄一個字符串是否出現過。對隊列中的每個元素都進行相同的操作,直到隊列為空還沒找到合法的字符串的話,那就返回空集,參見代碼如下:

 

解法一:

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

Longest Valid Parentheses

Generate Parentheses

Valid Parentheses

 

參考資料:

https://leetcode.com/problems/remove-invalid-parentheses/

https://leetcode.com/problems/remove-invalid-parentheses/discuss/75032/share-my-java-bfs-solution

https://leetcode.com/problems/remove-invalid-parentheses/discuss/75027/easy-short-concise-and-fast-java-dfs-3-ms-solution

https://leetcode.com/problems/remove-invalid-parentheses/discuss/75046/c-depth-limited-dfs-3ms-eliminate-duplicates-without-hashmap

 

LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM