[LC] 301. Remove Invalid Parentheses

 

这一题没有特别特别取巧的地方,两种方法去做。dfs和bfs,毕竟这是一道需要遍历所有情况的题目,目前看来没有更取巧的方法了。

如果采用dfs的方式,是需要预处理的。当然,dfs的本质就是遍历字符串,然后根据是否取当前字符分开两种情况进行dfs。但如果只是单纯的这样做性能会非常糟糕,因为会把所有valid的string都放进结果集而并不是特定长度的。所以我们需要预处理这个字符串,知道最长的valid string应该有多长,删掉多少左框或者右框。一开始我以为需要参考https://blog.csdn.net/chaochen1407/article/details/43230047 但发现我的理解是错的。那一题要找的是substring,这一题是随意删除括弧。所以需要的是维护两个变量,当前所需要删除的左括弧leftPar,当前所需要删除的右括弧rightPar。自左向右,遇到左括弧leftPar加一,遇到右括弧的时候如果leftPar大于0那么leftPar就减一,否则rightPar加一。这个和longest valid parentheses不一样,不需要两方向扫两次,没有意义。所以走到某个位置的时候,leftPar和rightPar的值就表示需要删除多少个左括弧和有括弧能得到一个最长的parentheses。举个例子,(())()())(( 这个字符串里,leftPar = 2, rightPar = 1,所以只需要删除两个左括弧和一个有括弧就能变成最长的parentheses(())()()了。

得到了leftPar和rightPar,我们就可以在原来的dfs基础上加停下来的机制。当我们在dfs的时候,删掉一个左括弧leftPar就减一,删掉一个有括弧rightPar就减一。如果leftPar或者rightPar等于0我们就不再走对应的删除括弧的dfs分枝。当字符已经是一个合理的parentheses而且leftPar和rightPar等于0的时候,我们就得到了一个答案了。在dfs里,比较容易去除duplicate答案的方式应该还是HashSet吧。给出代码如下:

    public List<String> removeInvalidParentheses(String s) {
        return this._dfsResolve(s);
    }
    
    private List<String> _dfsResolve(String s) {
        int leftToRem = 0, rightToRem = 0;
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '(') {
                leftToRem++;
            } else if (ch == ')') {
                if (leftToRem > 0) {
                    leftToRem--;
                } else {
                    rightToRem++;
                }
            }
        }
        
        Set<String> resultSet = new HashSet<>();
        _doDfs(resultSet, new StringBuilder(), s, leftToRem, rightToRem, 0, 0);
        
        return new LinkedList<String>(resultSet);
    }
    
    private void _doDfs(Set<String> resultSet, StringBuilder sb, String s, int leftToRem, int rightToRem, int open, int curPos) {
        if (curPos == s.length() && leftToRem == 0 && rightToRem == 0 && open == 0) {
            resultSet.add(sb.toString());
        } 
        
        if (leftToRem < 0 || rightToRem < 0 || open < 0 || curPos >= s.length()) {
            return;
        }
        
        int curLen = sb.length();
        char ch = s.charAt(curPos);
        if (ch == '(') {
            _doDfs(resultSet, sb, s, leftToRem - 1, rightToRem, open, curPos + 1);
            _doDfs(resultSet, sb.append(ch), s, leftToRem, rightToRem, open + 1, curPos + 1);
        } else if (ch == ')') {
            _doDfs(resultSet, sb, s, leftToRem, rightToRem - 1, open, curPos + 1);
            _doDfs(resultSet, sb.append(ch), s, leftToRem, rightToRem, open - 1, curPos + 1);            
        } else {
            _doDfs(resultSet, sb.append(ch), s, leftToRem, rightToRem, open, curPos + 1);                        
        }
        
        sb.setLength(curLen);
    }

这里有一点要注意的是关于StringBuilder的应用。它通过setLength来实际删除append的最后一个字母。然后用的是set去重复再重构一个list出来。

 

第二种办法就是bfs。bfs的原理其实不复杂,就是从头到尾遍历删除括号然后整理出不同的情况push到queue里面。然后遇到第一个valid的组合之后就不再push到queue直到把queue里面剩下的全部字符串全部验证完毕即可。
举例,在字符串(a(b())里,第一个遍历就会产生a(b()),(ab()),(a(b)), (a(b(), (a(b()。因为实际上(a(b()会出现多次,需要用到一个set来记录自己走过的情况。同时,连续的同样的括号也可以略过,作为去重复的环节我们可以认为我们同一层bfs都只删除连续括号中的第一个(这个思路接下来还可以简化dfs的做法)。当遇到第一个符合条件的子字符串的时候你就确定了答案的长度,然后你就不需要再分裂检查和push任何进queue里了。当queue为空或者push出第一个比结果长度更短的字符串的时候你就知道已经结束了,因为你已经知道结果长度了,queue里面剩下的都是更短的长度了。

根据上述描述,给出代码如下:

    public List<String> removeInvalidParentheses(String s) {
        return this._bfsResolve(s);
    }
    
    private List<String> _bfsResolve(String s) {
        Queue<String> bfsQ = new LinkedList<>();
        Set<String> visited = new HashSet<>();
        List<String> result = new LinkedList<>();
        int resLen = -1;
        
        visited.add(s);
        bfsQ.add(s);
        while (!bfsQ.isEmpty()) {
            String curS = bfsQ.poll();
            if (resLen != -1 && curS.length() < resLen) break;
            
            if (isValid(curS)) {
                resLen = curS.length();
                result.add(curS);
            }
            
            for (int i = 0; i < curS.length(); i++) {
                if (i > 0 && curS.charAt(i) == curS.charAt(i - 1)) continue;
                String nextS = curS.substring(0, i) + curS.substring(i + 1, curS.length());
                if (visited.contains(nextS)) continue;
                
                visited.add(nextS);
                bfsQ.add(nextS);
            }
        }
        
        return result;
    }
    
    public boolean isValid(String s) {
        int open = 0;
        for (int i = 0; i < s.length() && open >= 0; i++) {
            char ch = s.charAt(i);
            if (ch == '(') {
                open++;
            } else if (ch == ')') {
                open--;
            }
        }
        
        return open == 0;
    }

根据上面说的,去重复而且不利用hashset的方式有一种,就是连续的同一种括号,只删掉第一个作为分支,否则跳过。这样的去重复也可以应用在dfs上,这样dfs的解就不需要使用到HashSet。

    public List<String> removeInvalidParentheses(String s) {
        return this._dfsResolve(s);
    }
    
    private List<String> _dfsResolve(String s) {
        int leftToRem = 0, rightToRem = 0;
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '(') {
                leftToRem++;
            } else if (ch == ')') {
                if (leftToRem > 0) {
                    leftToRem--;
                } else {
                    rightToRem++;
                }
            }
        }
        
        List<String> result = new LinkedList<>();
        _doDfs(result, new StringBuilder(), s, leftToRem, rightToRem, 0, 0);
        return result;
    }
    
    private void _doDfs(List<String> result, StringBuilder sb, String s, int leftToRem, int rightToRem, int open, int curPos) {
        if (curPos == s.length() && leftToRem == 0 && rightToRem == 0 && open == 0) {
            result.add(sb.toString());
        } 
        
        if (leftToRem < 0 || rightToRem < 0 || open < 0 || curPos >= s.length()) {
            return;
        }
        
        int curLen = sb.length();
        char ch = s.charAt(curPos);
        boolean skip = curLen > 0 && sb.charAt(curLen - 1) == ch;
        if (ch == '(') {
            if(!skip) _doDfs(result, sb, s, leftToRem - 1, rightToRem, open, curPos + 1);
            _doDfs(result, sb.append(ch), s, leftToRem, rightToRem, open + 1, curPos + 1);
        } else if (ch == ')') {
            if(!skip) _doDfs(result, sb, s, leftToRem, rightToRem - 1, open, curPos + 1);
            _doDfs(result, sb.append(ch), s, leftToRem, rightToRem, open - 1, curPos + 1);            
        } else {
            _doDfs(result, sb.append(ch), s, leftToRem, rightToRem, open, curPos + 1);                        
        }
        
        sb.setLength(curLen);
    }

和最上面的代码对比起来,你可以发现我只是把HashSet改成了直接的List,另外,加了一个skip判定,就判定当前的括号是否一连串的连续相同的括号中的第一个,不是的话就不产生删除的分支。这样可以提高大约三倍的效率也不需要重新从set重构回list。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值