leetcode题解:第301题Remove Invalid Parentheses

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

文章目录

      • 解法一、BFS
        • 代码
      • 解法二、回溯
        • 代码
      • 解法三、有限制的回溯
        • 代码

解法一、BFS

这题要求所有的解,所以我们应当想到用搜索来解决,BFS的思想会比较直观:

  1. 由题目要求易知:所有的解都是由原始字符串删除了相同数量的括号得到的
  2. 对于删除的括号数量i,生成对应的所有的字符串,它们属于第i个level
  3. 对于每个level,判断其中的字符串是否有解:若有,则所有的解都来自于这个level;否则,进入到下一个level来寻找解

注意: 容易发现,每个level会有很多重复的字符串,所以我们需要用集合来进行去重。

如何判断一个字符串合法?可以通过左右括号的数量来判断。遍历过程中右括号的数量必须小于等于左括号的数量,最终左右括号数量必须相等,这才是一个合法的字符串。

代码

class Solution {
     
    public List<String> removeInvalidParentheses(String s) {
     
        List<String> ans = new LinkedList<String>();
        Queue<String> level = new LinkedList<String>();
        Set<String> visited = new HashSet<String>();
        level.offer(s);
        while (!level.isEmpty() && ans.isEmpty()) {
     
            int size = level.size();
            visited.clear();
            for (int i = 0; i < size; ++i) {
     
                s = level.poll();
                if (isValid(s)) {
     
                    ans.add(s);
                    continue;
                }
                for (int k = 0; k < s.length(); ++k) {
     
                    if (s.charAt(k) == '(' || s.charAt(k) == ')') {
     
                        String str = s.substring(0, k) + s.substring(k + 1);
                        if (!visited.contains(str)) {
     
                            visited.add(str);
                            level.offer(str);
                        }
                    }
                }
            }
        }
        return ans;
    }

    private boolean isValid(String s) {
     
        int left = 0, right = 0;
        for (int i = 0; i < s.length(); ++i) {
     
            char c = s.charAt(i);
            if (c == '(') left++;
            else if (c == ')') right++;
            if (left < right) return false;
        }
        return left == right;
    }
}

解法二、回溯

BFS的解法虽然简单,但是时间复杂度却比较高,因为我们对一个level中的所有字符串都进行了一遍遍历来判断是否合法,生成可能的字符串的过程也不够优雅。
DFS(回溯)的想法十分巧妙,它不以删除括号为目标,而是通过递归去遍历原始字符串,在递归过程中维护一个expression,最终的expression就是一个可能的解,需要判断其是否合法。 递归过程的细节:

  1. 对每一个字符c做两种选择:是否加入到expression中
  2. 维护加入到expression的左括号和右括号数量,在递归过程中,只有rightCount <= leftCount的expression才可能是一个合法的字符串,如果最终leftCount == rightCount,则expression是一个合法的字符串
  3. 维护已经删除的括号数量removed,由于题目要求删除最少数量的括号,所以对每一个合法的expression,还必须保证removed是最小的
  4. 这样生成的expression也是有重复的,所以同样需要用集合来进行去重

代码

class Solution {
     
    private Set<String> ans = new HashSet<String>();
    private int minRemoved = Integer.MAX_VALUE;

    public List<String> removeInvalidParentheses(String s) {
     
        backtrack(s, 0, 0, 0, new StringBuilder(), 0);
        return new ArrayList<String>(this.ans);
    }

    private void backtrack(String s, int index, int leftCount, int rightCount, StringBuilder expression, int removed) {
     
        if (index == s.length()) {
     
            if (leftCount == rightCount && removed <= minRemoved) {
     
                if (removed < minRemoved) {
     
                    ans.clear();
                    this.minRemoved = removed;
                }
                this.ans.add(expression.toString());
            }
        }
        else {
     
            char c = s.charAt(index);
            int length = expression.length();
            if (c != '(' && c != ')') {
     
                expression.append(c);
                backtrack(s, index + 1, leftCount, rightCount, expression, removed);
                expression.deleteCharAt(length);
            }
            // 注意这个else里会前后执行两次backtrack,
            // 前一次backtrack对expression造成的修改不应该影响第二次backtrack
            // 所以需要回溯,删去对expression的修改
            else {
     
                // 删掉这个括号
                backtrack(s, index + 1, leftCount, rightCount, expression, removed + 1);
                // 不删掉,即加入expression
                expression.append(c);
                if (c == '(') backtrack(s, index + 1, leftCount + 1, rightCount, expression, removed);
                else if (rightCount < leftCount) backtrack(s, index + 1, leftCount, rightCount + 1, expression, removed);
                expression.deleteCharAt(length);
            }
        }
    }
}

解法三、有限制的回溯

解法二的思想虽然巧妙地减少了判断字符串是否合法的时间,但由于会生成非常多的expression,时间复杂度同样较高(BFS则不会生成太多,若在某一个level找到解,则停止进入下一个level)。
为什么解法二会生成非常多的expression?因为我们没有对删除的括号数量进行限制,而是先删除,再比较删除数量的大小,这样自然就会导致很多生成的expression其对应的删除数量很大,并不是我们想要的解。简单来说,就是没有充分剪枝。
如果我们能从原始字符串中找出需要删除的左右括号数量leftRemrightRem,那么在回溯过程中就能通过限制删除括号的数量,进行很多剪枝,大量地减少递归的次数。
我们可以通过一次遍历来找到leftRemrightRem

  1. 如果c == '(',则leftRem++
  2. 如果c == ')',则需要判断它是否有对应的(,如果有,则是合法的,如果没有,则是非法的。具体地来说,如果leftRem == 0,则rightRem++;否则leftRem--
  3. 最终的leftRemrightRem就是我们想要的结果

代码

class Solution {
     
    private Set<String> ans = new HashSet<String>();

    public List<String> removeInvalidParentheses(String s) {
     
        // 统计应该删除多少左括号和右括号
        int leftRem = 0, rightRem = 0;
        for (int i = 0; i < s.length(); ++i) {
     
            char c = s.charAt(i);
            if (c == '(') leftRem++;
            else if (c == ')') {
     
                if (leftRem == 0) rightRem++;
                else leftRem--;
            }
        }
        backtrack(s, 0, 0, 0, leftRem, rightRem, new StringBuilder());
        return new ArrayList(this.ans);
    }

    private void backtrack(String s, int index, int leftCount, int rightCount, int leftRem, int rightRem, StringBuilder expression) {
     
        if (index == s.length()) {
     
            if (leftCount == rightCount) this.ans.add(expression.toString());
        }
        else {
     
            char c = s.charAt(index);
            int length = expression.length();
            if (c != '(' && c != ')') {
     
                expression.append(c);
                backtrack(s, index + 1, leftCount, rightCount, leftRem, rightRem, expression);
                expression.deleteCharAt(length);
            }
            // 注意这个else里会前后执行两次backtrack,
            // 前一次backtrack对expression造成的修改不应该影响第二次backtrack
            // 所以需要回溯,删去对expression的修改
            else {
     
                // 删掉这个括号
                if (c == '(' && leftRem > 0) backtrack(s, index + 1, leftCount, rightCount, leftRem - 1, rightRem, expression);
                else if (rightRem > 0) backtrack(s, index + 1, leftCount, rightCount, leftRem, rightRem - 1, expression);
                // 不删掉,即加入expression
                expression.append(c);
                if (c == '(') backtrack(s, index + 1, leftCount + 1, rightCount, leftRem, rightRem, expression);
                else if (rightCount < leftCount) backtrack(s, index + 1, leftCount, rightCount + 1, leftRem, rightRem, expression);
                expression.deleteCharAt(length);
            }
        }
    }
}

你可能感兴趣的:(算法)