1. 括号生成
(0)回溯(最容易懂的一版)
- 剩余左右括号数相等,下一个只能用左括号
- 剩余左括号小于右括号,下一个可以用左括号也可以用右括号
class Solution {
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
if (n <= 0)
return res;
backTracking("", n, n);
return res;
}
private void backTracking(String str, int left, int right){
if (left == 0 && right == 0){
res.add(str);
return;
}
if (left == right) {
//剩余左右括号数相等,下一个只能用左括号
backTracking(str + "(", left - 1, right);
} else if (left < right) {
//剩余左括号小于右括号,下一个可以用左括号也可以用右括号
if (left > 0) {
backTracking(str + "(", left - 1, right);
}
backTracking(str + ")", left, right - 1);
}
}
}
(1)回溯法(减法)
- 这里用的不是全局变量,每一次会覆盖前面的选择,相当于撤销 。但是用的如果是全局变量(类似迷宫访问那种,就要显式撤销)
- 主要是跟字符串的特点有关哈,Java 里 + 生成了新的字符串,每次往下面传递的时候,都是新字符串。因此在搜索的时候不用回溯。
- 可以想象搜索遍历的问题其实就像是做实验,每一次实验都用新的实验材料,那么做完了就废弃了。但是如果只使用一份材料,在做完一次以后,一定需要将它恢复成原样(就是这里「回溯」的意思),才可以做下一次尝试。
- 下面也给出了 StringBuilder 全程只使用一份变量去搜索的做法,对比这两种解法
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;
}
StringBuilder path = new StringBuilder();
dfs(path, n, n, res);
return res;
}
/**
* @param path 从根结点到任意结点的路径,全程只使用一份
* @param left 左括号还有几个可以使用
* @param right 右括号还有几个可以使用
* @param res 结果list
*/
private void dfs(StringBuilder path, int left, int right, List<String> res) {
if (left == 0 && right == 0) {
// path.toString() 生成了一个新的字符串,相当于做了一次拷贝
res.add(path.toString());
return;
}
// 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
// 只要当前已使用闭括号 大于 开括号了 ,直接就是无效的括号组合(翻译过来就是 left > right)
if (left > right) {
return;
}
if (left > 0) {
path.append("(");
dfs(path, left - 1, right, res);
path.deleteCharAt(path.length() - 1);
}
if (right > 0) {
path.append(")");
dfs(path, left, right - 1, res);
path.deleteCharAt(path.length() - 1);
}
}
}
其实可以直接这样的:
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;
}
//这里用的不是全局变量,每一次会覆盖前面的选择,相当于撤销 。但是用的如果是全局变量(类似迷宫访问那种,就要显式撤销)
//主要是跟字符串的特点有关哈,Java 里 + 生成了新的字符串,每次往下面传递的时候,都是新字符串。因此在搜索的时候不用回溯。
//可以想象搜索遍历的问题其实就像是做实验,每一次实验都用新的实验材料,那么做完了就废弃了。但是如果只使用一份材料,在做完一次以后,一定需要将它恢复成原样(就是这里「回溯」的意思),才可以做下一次尝试。
//上面也给出了 StringBuilder 全程只使用一份变量去搜索的做法,对比这两种解法
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);
}
}
}
(2)回溯法(加法)
public class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
if (n <= 0)
return res;
dfs(n, "", res, 0, 0);
return res;
}
/**
* @param path 从根结点到任意结点的路径,全程只使用一份
* @param open 左括号使用了多少个
* @param close 右括号使用了多少个
* @param res 结果list
*/
private void dfs(int n, String path, List<String> res, int open, int close) {
if (open > n || close > open) //左括号使用大于n,当前已使用右括号数量大于左括号
return;
if (path.length() == 2 * n) {
res.add(path);
return;
}
dfs(n, path + "(", res, open + 1, close);
dfs(n, path + ")", res, open, close + 1);
}
}
换种写法也是可以的,思路跟上面的一致:
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
if (n <= 0) return res;
dfs(n, "", res, 0, 0);
return res;
}
private void dfs(int n, String path, List<String> res, int open, int close) {
if (path.length() == 2 * n) {
res.add(path);
return;
}
if (open < n) { //左括号小于n时,才能遍历左边
dfs(n, path + "(", res, open + 1, close);
}
if (close < open) { //右括号小于左括号时,才能遍历右边
dfs(n, path + ")", res, open, close + 1);
}
}
(3)广度优先遍历
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;
}
}
2. 有效的括号字符串
(1)暴力 (超时)
- 深度优先搜索,直接暴搜
- 先考虑不带星号的情况,这时候我们只要分别统计左括号和右括号的数量,最后判断左括号是否等于右括号即可
- 如果中间遇到了右括号比左括号数量多了,说明先出现了未经匹配的右括号,直接返回
- 星号,可以代表左括号、右括号、空,这三种只要有一种满足最后左右括号相等即可,那么,很简单,我们就尝试着让它分别代表左括号、右括号、空往下搜索即可
class Solution {
public boolean checkValidString(String s) {
if (s.isEmpty())
return true;
int n = s.length();
return dfs(s, 0 , 0);
}
private boolean dfs (String s, int i, int count) {
int n = s.length();
if (count < 0)
return false;
if (i >= n) {
//搜到最后一位且所有括号匹配上
if (count == 0)
return true;
return false;
}
//分别判断左右括号与*
boolean res;
if (s.charAt(i) == '(')
res = dfs(s, i + 1, count + 1);
else if (s.charAt(i) == ')')
res = dfs(s, i + 1, count - 1);
else
res = dfs(s, i + 1, count) || dfs(s, i + 1, count + 1) || dfs(s, i + 1, count - 1);
return res;
}
}
(2)记忆化搜索
可以发现,存在 * 的时候会出现大量的重复计算。
- 比如,以 ****())) 为例,计算到第四个 * 的时候,假设 count=1 ,这个 1 有可能是前面任意一个位置贡献的,也有可能是前面三个位置有任意两个位置是左括号,另一个位置是右括号,但是不管前面怎么变化,对于后面的计算它们的结果是一样的,所以,我们可以在暴搜的基础上加上记忆化,所以,又称为 记忆化搜索。
class Solution {
public boolean checkValidString(String s) {
if (s.isEmpty())
return true;
int n = s.length();
int[][] memo = new int[n][n + 1];
return dfs(s, 0 , 0, memo);
}
private boolean dfs (String s, int i, int count, int[][] memo) {
int n = s.length();
if (count < 0)
return false;
if (i >= n) {
//搜到最后一位且所有括号匹配上
if (count == 0)
return true;
return false;
}
//如果该情况已被记录
if (memo[i][count] != 0) {
return memo[i][count] == 1;
}
//分别判断左右括号与*
boolean res;
if (s.charAt(i) == '(')
res = dfs(s, i + 1, count + 1, memo);
else if (s.charAt(i) == ')')
res = dfs(s, i + 1, count - 1, memo);
else
res = dfs(s, i + 1, count, memo) || dfs(s, i + 1, count + 1, memo) || dfs(s, i + 1, count - 1, memo);
memo[i][count] = res ? 1 : -1;
return res;
}
}
(3)动态规划
class Solution {
public boolean checkValidString(String s) {
int n = s.length();
boolean[][] dp = new boolean[n + 1][n + 1];
dp[0][0] = true;
// i 表示前i个,不是s的下标
for (int i = 1; i <= n; i++) {
// 前i个字符最多只会有i个左括号
for (int c = 0; c <= i; c++) {
char ch = s.charAt(i - 1);
if (ch == '(') {
// 当前为左括号了,所以,左括号的数量肯定不会为0
if (c > 0) {
dp[i][c] = dp[i - 1][c - 1];
}
} else if (ch == ')') {
if (c + 1 < n + 1) {
dp[i][c] = dp[i - 1][c + 1];
}
} else if (ch == '*') {
dp[i][c] = dp[i - 1][c];
if (c > 0) {
dp[i][c] = dp[i][c] || dp[i - 1][c - 1];
}
if (c + 1 < n + 1) {
dp[i][c] = dp[i][c] || dp[i - 1][c + 1];
}
}
}
}
return dp[n][0];
}
}