22. 括号生成

106 阅读2分钟

思路

DFS

  • 每层供选的元素是剩余的括号,终止条件为剩余0个括号
  • 控制括号生成顺序有效:) 剩余数量必须大于等于 (剩余数量

回溯与不回溯的区别

「回溯算法」强调了在状态空间特别大的时候,只用一份状态变量去搜索所有可能的状态,在搜索到符合条件的解的时候,通常会做一个拷贝,这就是为什么经常在递归终止条件的时候,有 res.add(new ArrayList<>(path)); 这样的代码。正是因为全程使用一份状态变量,因此它就有「恢复现场」和「撤销选择」的需要。

每次搜索的的过程,也可以直接复制一个String,见方法二。

leetcode-cn.com/problems/ge…

方法一:DFS + 回溯

全程使用一份状态变量SB path 来记录路径。

  • 需要回溯
  • 添加到res时需要new一下 新写法
class Solution {
    List<String> path = new ArrayList<>();
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        dfs(n, n);
        return res;
    }

    // leftNum为当前左括号剩余数量,rightNum为右括号剩余数量
    public void dfs(int leftNum, int rightNum) {
        if (leftNum == 0 && rightNum == 0) {//左右括号都用完了,找到了正确的排列
            String tmp = "";
            for (int i = 0; i < path.size(); i++) {
                tmp += path.get(i);
            }
            res.add(tmp);
        }
        if (leftNum > rightNum) {// "())", ")"
            return;
        }

        if (leftNum != 0) {
            path.add("(");
            dfs(leftNum - 1, rightNum);
            path.remove(path.size() - 1);
        }
        if (rightNum != 0) {
            path.add(")");
            dfs(leftNum, rightNum - 1);
            path.remove(path.size() - 1);
        }
    }
}

第一次写法

public class Solution {
    StringBuilder path = new StringBuilder();
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        if (n == 0) {
            return res;
        }
        dfs(n, n);
        return res;
    }
    //left: 剩余的"("个数,right:剩余的")"个数
    public void dfs(int left, int right) {
        if (left == 0 && right == 0) {//剩余括号为0,结束
            res.add(path.toString());//SB的toStr相当于new了一个
            return;
        }
        if (left > right) {//剪枝,为了保证顺序有效
            return;
        }
        if (left > 0) {//当"("还有剩余时
            path.append("(");
            dfs(left - 1, right);
            path.deleteCharAt(path.length() - 1);//回溯
        }
        if (right > 0) {//当")"还有剩余时
            path.append(")");
            dfs(left, right - 1);
            path.deleteCharAt(path.length() - 1);//回溯
        }
    }
}

方法二:DFS 不回溯

每次搜索都使用新的String变量来记录路径,+生成了新的字符串,每次往下面传递的时候,都是新字符串。因此在搜索的时候不用回溯。

  • 无需回溯
  • 直接添加到res中。
public class Solution {
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        if (n == 0) {
            return res;
        }
        dfs("", n, n);//用string来记录路径
        return res;
    }
    public void dfs(String curStr, int left, int right) {
        // 因为每一次尝试,都使用新的字符串变量而不是引用,所以无需回溯
        // 在递归终止的时候,直接把它添加到结果集即可。
        if (left == 0 && right == 0) {
            res.add(curStr);
            return;
        }
        // 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
        if (left > right) {
            return;
        }
        if (left > 0) {
            dfs(curStr + "(", left - 1, right);
        }
        if (right > 0) {
            dfs(curStr + ")", left, right - 1);
        }
    }
}

方法三 :找位置插入,去重。

太憨了,别这么写

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        result.add("()");
        if (n == 1) {
            return result;
        }
        for (int i = 2; i <= n; i++) {
            List<String> tmp = new ArrayList<>();
            for (int j = 0; j < result.size(); j++ ) {
                StringBuilder sb = new StringBuilder(result.get(j));
                for (int k = 0; k < sb.length(); k++) {
                    sb.insert(k, "()");
                    tmp.add(sb.toString());
                    sb = new StringBuilder(result.get(j));
                }
            }
            result = tmp.stream().distinct().collect(Collectors.toList());
        }
        Collections.sort(result);
        return result;
    }
}