1. 程式人生 > >[LeetCode] Remove Invalid Parentheses 移除非法括號

[LeetCode] 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 ).

Examples:

"()())()" -> ["()()()", "(())()"]
"(a)())()" -> ["(a)()()", "(a())()"]
")(" -> [""]

Credits:
Special thanks to @hpplayer for adding this problem and creating all test cases.

Subscribe to see which companies asked this question

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

解法一:

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);
    }
};

類似題目:

參考資料: