题目解释
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例
输入:n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
解题思路
方法一:深度优先遍历
我们以 n = 2 为例,画树形结构图。方法是 “做减法”。
画图以后,可以分析出的结论:
-
当前左右括号都有大于 0 个可以使用的时候,才产生分支;
-
产生左分支的时候,只看当前是否还有左括号可以使用;
-
产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以产生分支;
-
在左边和右边剩余的括号数都等于 0 的时候结算
代码实现 1
// java方式实现
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 做减法
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
// 特判
if (n == 0) {
return res;
}
// 执行深度优先遍历,搜索可能的结果
dfs("", n, n, res);
return res;
}
/**
* @param curStr 当前递归得到的结果
* @param left 左括号还有几个可以使用
* @param right 右括号还有几个可以使用
* @param res 结果集
*/
private void dfs(String curStr, int left, int right, List<String> res) {
// 因为每一次尝试,都使用新的字符串变量,所以无需回溯
// 在递归终止的时候,直接把它添加到结果集即可,注意与「力扣」第 46 题、第 39 题区分
if (left == 0 && right == 0) {
res.add(curStr);
return;
}
// 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
if (left > right) {
return;
}
if (left > 0) {
dfs(curStr + "(", left - 1, right, res);
}
if (right > 0) {
dfs(curStr + ")", left, right - 1, res);
}
}
}
# Python方式实现
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []
cur_str = ''
def dfs(cur_str, left, right):
"""
:param cur_str: 从根结点到叶子结点的路径字符串
:param left: 左括号还可以使用的个数
:param right: 右括号还可以使用的个数
:return:
"""
if left == 0 and right == 0:
res.append(cur_str)
return
if right < left:
return
if left > 0:
dfs(cur_str + '(', left - 1, right)
if right > 0:
dfs(cur_str + ')', left, right - 1)
dfs(cur_str, n, n)
return res
我们运行 n = 2
的情况,得到结果 [(()), ()()]
,说明分析的结果是正确的。
如果我们不用减法,使用加法,即 left 表示“左括号还有几个没有用掉”,right 表示“右括号还有几个没有用掉”,可以画出另一棵递归树。
代码实现 2
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 做加法
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
// 特判
if (n == 0) {
return res;
}
dfs("", 0, 0, n, res);
return res;
}
/**
* @param curStr 当前递归得到的结果
* @param left 左括号已经用了几个
* @param right 右括号已经用了几个
* @param n 左括号、右括号一共得用几个
* @param res 结果集
*/
private void dfs(String curStr, int left, int right, int n, List<String> res) {
if (left == n && right == n) {
res.add(curStr);
return;
}
// 剪枝
if (left < right) {
return;
}
if (left < n) {
dfs(curStr + "(", left + 1, right, n, res);
}
if (right < n) {
dfs(curStr + ")", left, right + 1, n, res);
}
}
}
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []
cur_str = ''
def dfs(cur_str, left, right, n):
"""
:param cur_str: 从根结点到叶子结点的路径字符串
:param left: 左括号已经使用的个数
:param right: 右括号已经使用的个数
:return:
"""
if left == n and right == n:
res.append(cur_str)
return
if left < right:
return
if left < n:
dfs(cur_str + '(', left + 1, right, n)
if right < n:
dfs(cur_str + ')', left, right + 1, n)
dfs(cur_str, 0, 0, n)
return res
方法二:广度优先遍历
通过编写广度优先遍历的代码,读者可以体会一下,为什么搜索几乎都是用深度优先遍历(回溯算法)。
广度优先遍历,得程序员自己编写结点类,显示使用队列这个数据结构。深度优先遍历的时候,就可以直接使用系统栈,在递归方法执行完成的时候,系统栈顶就把我们所需要的状态信息直接弹出,而无须编写结点类和显示使用栈。
下面的代码,读者可以把 Queue 换成 Stack,提交以后,也可以得到 Accept。
读者可以通过比较:
1、广度优先遍历;
2、自己使用栈编写深度优先遍历;
3、使用系统栈的深度优先遍历(回溯算法)。
来理解 “回溯算法” 作为一种 “搜索算法” 的合理性。
还是上面的题解配图(1),使用广度优先遍历,结果集都在最后一层,即叶子结点处得到所有的结果集,编写代码如下。
代码实现 3:
(前 2 个 Java 代码写法没有本质不同,仅供参考。第 3 个 Java 代码仅仅是把 Queue 换成了 Stack ,广度优先遍历就改成了深度优先遍历。)
// 版本一
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Solution {
class Node {
/**
* 当前得到的字符串
*/
private String res;
/**
* 剩余左括号数量
*/
private int left;
/**
* 剩余右括号数量
*/
private int right;
public Node(String str, int left, int right) {
this.res = str;
this.left = left;
this.right = right;
}
}
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
if (n == 0) {
return res;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(new Node("", n, n));
while (!queue.isEmpty()) {
Node curNode = queue.poll();
if (curNode.left == 0 && curNode.right == 0) {
res.add(curNode.res);
}
if (curNode.left > 0) {
queue.offer(new Node(curNode.res + "(", curNode.left - 1, curNode.right));
}
if (curNode.right > 0 && curNode.left < curNode.right) {
queue.offer(new Node(curNode.res + ")", curNode.left, curNode.right - 1));
}
}
return res;
}
}
// 版本二
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Solution {
class Node {
/**
* 当前得到的字符串
*/
private String res;
/**
* 剩余左括号数量
*/
private int left;
/**
* 剩余右括号数量
*/
private int right;
public Node(String res, int left, int right) {
this.res = res;
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"res='" + res + '\'' +
", left=" + left +
", right=" + right +
'}';
}
}
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
if (n == 0) {
return res;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(new Node("", n, n));
// 总共需要拼凑的字符总数是 2 * n
n = 2 * n;
while (n > 0) {
int size = queue.size();
for (int i = 0; i < size; i++) {
Node curNode = queue.poll();
if (curNode.left > 0) {
queue.offer(new Node(curNode.res + "(", curNode.left - 1, curNode.right));
}
if (curNode.right > 0 && curNode.left < curNode.right) {
queue.offer(new Node(curNode.res + ")", curNode.left, curNode.right - 1));
}
}
n--;
}
// 最后一层就是题目要求的结果集
while (!queue.isEmpty()) {
res.add(queue.poll().res);
}
return res;
}
}
// 版本三
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
public class Solution {
class Node {
/**
* 当前得到的字符串
*/
private String res;
/**
* 剩余左括号数量
*/
private int left;
/**
* 剩余右括号数量
*/
private int right;
public Node(String str, int left, int right) {
this.res = str;
this.left = left;
this.right = right;
}
}
// 注意:这是深度优先遍历
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
if (n == 0) {
return res;
}
// 查看了 Stack 源码,官方推荐使用 Deque 对象,
// 注意:只使用栈相关的接口,即只使用 `addLast()` 和 `removeLast()`
Deque<Node> stack = new ArrayDeque<>();
stack.addLast(new Node("", n, n));
while (!stack.isEmpty()) {
Node curNode = stack.removeLast();
if (curNode.left == 0 && curNode.right == 0) {
res.add(curNode.res);
}
if (curNode.left > 0) {
stack.addLast(new Node(curNode.res + "(", curNode.left - 1, curNode.right));
}
if (curNode.right > 0 && curNode.left < curNode.right) {
stack.addLast(new Node(curNode.res + ")", curNode.left, curNode.right - 1));
}
}
return res;
}
}
方法三:动态规划
第 1 步:定义状态 dp[i]
:使用 i
对括号能够生成的组合。
注意:每一个状态都是列表的形式。
第 2 步:状态转移方程:
i
对括号的一个组合,在i-1
i 对括号的基础上得到,这是思考 “状态转移方程” 的基础;i
对括号的一个组合,一定以左括号 “(” 开始,不一定以 “)” 结尾。为此,我们可以枚举新的右括号 “)” 可能所处的位置,得到所有的组合;
枚举的方式就是枚举左括号 “(” 和右括号 “)” 中间可能的合法的括号对数,而剩下的合法的括号对数在与第一个左括号 “(” 配对的右括号 “)” 的后面,这就用到了以前的状态。
状态转移方程是:
dp[i] = "(" + dp[可能的括号对数] + ")" + dp[剩下的括号对数]
“可能的括号对数” 与 “剩下的括号对数” 之和得为 i-1
,故 “可能的括号对数” j
可以从 0
开始,最多不能超过 i
, 即 i-1
;
“剩下的括号对数” + j = i - 1
,故 “剩下的括号对数” = i - j - 1
。
dp[i] = "(" + dp[j] + ")" + dp[i- j - 1] , j = 0, 1, ..., i - 1
第 3 步: 思考初始状态和输出:
- 初始状态:因为我们需要
0
对括号这种状态,因此状态数组dp
从0
开始,0
个括号当然就是[""]
。 - 输出:
dp[n]
。
这个方法暂且就叫它动态规划,这么用也是很神奇的,它有下面两个特点:
1、自底向上:从小规模问题开始,逐渐得到大规模问题的解集;
2、无后效性:后面的结果的得到,不会影响到前面的结果。
代码实现4
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 把结果集保存在动态规划的数组里
public List<String> generateParenthesis(int n) {
if (n == 0) {
return new ArrayList<>();
}
// 这里 dp 数组我们把它变成列表的样子,方便调用而已
List<List<String>> dp = new ArrayList<>(n);
List<String> dp0 = new ArrayList<>();
dp0.add("");
dp.add(dp0);
for (int i = 1; i <= n; i++) {
List<String> cur = new ArrayList<>();
for (int j = 0; j < i; j++) {
List<String> str1 = dp.get(j);
List<String> str2 = dp.get(i - 1 - j);
for (String s1 : str1) {
for (String s2 : str2) {
// 枚举右括号的位置
cur.add("(" + s1 + ")" + s2);
}
}
}
dp.add(cur);
}
return dp.get(n);
}
}
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
if n == 0:
return []
dp = [None for _ in range(n + 1)]
dp[0] = [""]
for i in range(1, n + 1):
cur = []
for j in range(i):
left = dp[j]
right = dp[i - j - 1]
for s1 in left:
for s2 in right:
cur.append("(" + s1 + ")" + s2)
dp[i] = cur
return dp[n]