数组

大家好,我是老马。

今天我们一起来学习一下括号生成

22. 括号生成 generate-parentheses

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3 输出:[”((()))”,”(()())”,”(())()”,”()(())”,”()()()”] 示例 2:

输入:n = 1 输出:[”()”]

提示:

1 <= n <= 8

v1-回溯

思路

我们可以用回溯的方式来解决。

最笨的方法是我们不做任何的剪枝。

然后每次取所有的 () 的可能性,在满的时候,判断是否合法。

实现

    public List<String> generateParenthesis(int n) {
        char[] chars = new char[]{'(', ')'};
        List<String> res = new ArrayList<>();
        StringBuilder path = new StringBuilder();
        backtrack(chars, res, path, n * 2);
        return res;
    }

    private void backtrack(char[] chars, List<String> res, StringBuilder path, int targetLen) {
        if (path.length() == targetLen) {
            if (isValid(path)) res.add(path.toString());
            return;
        }

        for (char c : chars) {
            path.append(c);
            backtrack(chars, res, path, targetLen);
            path.deleteCharAt(path.length() - 1);
        }
    }

    private boolean isValid(StringBuilder path) {
        int balance = 0;
        for (int i = 0; i < path.length(); i++) {
            char c = path.charAt(i);
            if (c == '(') balance++;
            else {
                balance--;
                if (balance < 0) return false; // 右括号多于左括号,非法
            }
        }
        return balance == 0;
    }

效果

6ms 击败 6.67%

反思

当然,实际上我们有时候不需要一直取多余的 ( 或者 )

如果我们知道已经取了多少个了,就可以知道是否需要。从而提升效率。

v2-回溯-剪枝

思路

我们用两个变量:

leftRemain      // ( 剩余
rightRemain     // ) 剩余

这样如果 remain 中任何一个不足的时候,就可以直接剪枝跳过了。

java

    public List<String> generateParenthesis(int n) {
        char[] chars = new char[]{'(', ')'};
        List<String> res = new ArrayList<>();
        StringBuilder path = new StringBuilder();

        backtrack(chars, res, path, n, n);
        return res;
    }

    private void backtrack(char[] chars, List<String> res, StringBuilder path, int leftRemain, int rightRemain) {
        if (leftRemain == 0 && rightRemain == 0) {
            if (isValid(path)) {
                res.add(path.toString());
            } 

            return;
        }

        for (char c : chars) {
            if(leftRemain == 0 && c == '(') {
                continue;
            }
            if(rightRemain == 0 && c == ')') {
                continue;
            }

            path.append(c);

            if(c == '(') {
                leftRemain--;
            }
            if(c == ')') {
                rightRemain--;
            }
            backtrack(chars, res, path, leftRemain, rightRemain);

            // 撤销
            if(c == '(') {
                leftRemain++;
            }
            if(c == ')') {
                rightRemain++;
            }
            path.deleteCharAt(path.length() - 1);
        }
    }

    private boolean isValid(StringBuilder path) {
        int balance = 0;
        for (int i = 0; i < path.length(); i++) {
            char c = path.charAt(i);
            if (c == '(') balance++;
            else {
                balance--;
                if (balance < 0) return false; // 右括号多于左括号,非法
            }
        }
        return balance == 0;
    }

效果

3ms 击败 10.18%

反思

略有进步

我们还能更快吗?

v3-回溯剪枝改良

思路

其实我们可以进一步优化剪枝策略。

1)路径表示

回溯中的 path 就是一条括号序列

每次决策:加 ‘(‘ 还是 ‘)’

2)状态记录

左括号数 open

右括号数 close

核心:只有知道已经用了多少括号,才能判断下一步是否合法

3)剪枝规则

open < n → 可以加 ‘(‘ close < open → 可以加 ‘)’

每一步选择都受约束 → 递归树里不会出现非法组合

4)终止条件

path.length() == 2*n → 组合完成

实现

public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        StringBuilder path = new StringBuilder();
        backtrack(res, path, 0, 0, n);
        return res;
    }

    private void backtrack(List<String> res, StringBuilder path, int open, int close, int n) {
        if ((open + close) == 2*n) {
            res.add(path.toString());
            return;
        }

        // ( 还能加,那就加
        if(open < n) {
            path.append('(');
            backtrack(res, path, open+1, close, n);
            path.deleteCharAt(path.length()-1);
        }

        // close < open
        if(close < open) {
            path.append(')');
            backtrack(res, path, open, close+1, n);
            path.deleteCharAt(path.length()-1);
        }
    }

效果

0ms 100%