文章目录
前言
本文记录力扣Hot100里面关于回溯的四道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解
一、括号生成
1.题目
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
- 1 <= n <= 8
2.代码
java
class Solution {
// 生成n对有效括号
public List<String> generateParenthesis(int n) {
List<String> ans = new ArrayList<>();
// 回溯:结果、当前字符串、左括号数、右括号数、最大对数n
backtrack(ans, new StringBuilder(), 0, 0, n);
return ans;
}
// 回溯函数
void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
// 终止:字符串长度达到2n,加入结果
if (cur.length() == max * 2) {
ans.add(cur.toString());
return;
}
// 左括号没满,添加左括号
if (open < max) {
cur.append('(');
backtrack(ans, cur, open + 1, close, max);
cur.deleteCharAt(cur.length() - 1); // 回溯撤销
}
// 右括号少于左括号,添加右括号
if (close < open) {
cur.append(')');
backtrack(ans, cur, open, close + 1, max);
cur.deleteCharAt(cur.length() - 1); // 回溯撤销
}
}
}
3.例子
以 n=3 为例:
生成 3对有效括号
正确答案:
["((()))", "(()())", "(())()", "()(())", "()()()"]
- 左括号
(:只要少于3个,就能加 - 右括号
):必须少于左括号,才能加 - 长度到 6 就是一个答案
初始状态:
cur = "",open=0,close=0
路径 1:一直加左,直到加满
-
open < 3→ 加(
cur=(, open=1, close=0 -
open < 3→ 加(
cur=((, open=2, close=0 -
open < 3→ 加(
cur=(((, open=3, close=0
左括号满了,只能加右括号
-
close < open→ 加)
cur=((() -
close < open→ 加)
cur=((()) -
close < open→ 加)
cur=((()))
长度=6 → 加入答案!
开始回溯(撤销,尝试另一条路)
删掉最后一个 ) → ((())
再删 → ((()
再删 → ((
回到这里:cur=( (,open=2,close=0
路径 2:左2个 → 加右,再加左
-
close < open→ 加)
cur=((),open=2, close=1 -
open < 3→ 加(
cur=(()(,open=3, close=1 -
close < open→ 加)
cur=(()) -
close < open→ 加)
cur=(()())
长度=6 → 加入答案!
再回溯
一直撤销,回到:cur=( (,open=2,close=0
继续走:加右 → 再加右
路径 3:(()) 再加左
-
cur=(()),open=2, close=2 -
open <3→ 加(
cur=(())( -
补
)
cur=(())()
加入答案!
再回溯,回到最开始:cur=,open=1
路径 4:先 () 再 (( ))
-
cur=(),open=1, close=1 -
加
(→()( -
加
(→()(( -
加
)→()(())
加入答案!
路径 5:()()()
- 一路交替加
()()()
加入答案!
最终结果
["((()))", "(()())", "(())()", "()(())", "()()()"]
注意:
- 左括号能加就加
- 右括号必须比左少才能加
- 一条路走到底 → 记录答案 → 回头撤销 → 走另一条路
二、单词搜索
1.题目
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序 ,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻 或垂直相邻 的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"
输出:true
示例 2:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE"
输出:true
示例 3:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB"
输出:false
提示:
- m == board.length
- n = board[i].length
- 1 <= m, n <= 6
- 1 <= word.length <= 15
- board 和 word 仅由大小写英文字母组成
2.代码
java
class Solution {
public boolean exist(char[][] board, String word) {
int rows = board.length; // 矩阵的行数
int cols = board[0].length; // 矩阵的列数
boolean[][] visited = new boolean[rows][cols]; // 标记矩阵元素是否被访问过
// 遍历矩阵的每个位置,作为DFS的起始点
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 若当前位置字符DFS后能找到匹配的答案,直接返回true
if (dfs(board, word, i, j, 0, visited)) {
return true;
}
}
}
// 所有位置都尝试过,未找到匹配路径
return false;
}
private boolean dfs(char[][] board, String word, int row, int col, int index, boolean[][] visited) {
// 终止条件:单词所有字符匹配完成
if (index == word.length()) {
return true;
}
int rows = board.length;
int cols = board[0].length;
// 无效情况判断:越界、字符不匹配、已访问过 → 此路径无效
if (row < 0 || row >= rows || col < 0 || col >= cols
|| board[row][col] != word.charAt(index)
|| visited[row][col]) {
return false;
}
// 标记当前位置为已访问,避免同一路径重复使用
visited[row][col] = true;
// 依次尝试向上、下、左、右四个方向递归探索
boolean up = dfs(board, word, row - 1, col, index + 1, visited); // 向上
boolean down = dfs(board, word, row + 1, col, index + 1, visited); // 向下
boolean left = dfs(board, word, row, col - 1, index + 1, visited); // 向左
boolean right = dfs(board, word, row, col + 1, index + 1, visited); // 向右
// 只要有一个方向匹配成功,整体结果为true
boolean found = up || down || left || right;
// 回溯:撤销当前位置的访问标记,供其他路径复用
visited[row][col] = false;
return found;
}
}
3.例子
输入信息
board = [
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
word = "ABCCED"
单词下标:
0:A, 1:B, 2:C, 3:C, 4:E, 5:D
1. 遍历起点,找到 (0,0) 字符为 A
进入DFS,index = 0
字符匹配,标记 visited[0][0] = true
向上下左右搜索下一个字符 index=1(B)
2. 从 (0,0) 向右走到 (0,1)
字符为 B,匹配 index=1
标记 visited[0][1] = true
搜索下一个字符 index=2(C)
3. 从 (0,1) 向右走到 (0,2)
字符为 C,匹配 index=2
标记 visited[0][2] = true
搜索下一个字符 index=3(C)
4. 从 (0,2) 向下走到 (1,2)
字符为 C,匹配 index=3
标记 visited[1][2] = true
搜索下一个字符 index=4(E)
5. 从 (1,2) 向下走到 (2,2)
字符为 E,匹配 index=4
标记 visited[2][2] = true
搜索最后一个字符 index=5(D)
6. 从 (2,2) 向左走到 (2,1)
字符为 D,匹配 index=5
标记 visited[2][1] = true
7. 进入下一层递归
此时 index = 6,等于单词长度,返回 true
最终路径
(0,0)A → (0,1)B → (0,2)C → (1,2)C → (2,2)E → (2,1)D
匹配成功,返回 true
回溯逻辑说明
- 每走到一个格子,先标记为已访问
- 向四个方向递归查找下一个字符
- 无论查找成功或失败,都会取消当前标记,恢复状态,供其他路径使用
三、分割回文串
1.题目
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
提示:
- 1 <= s.length <= 16
- s 仅由小写英文字母组成
2.代码
java
class Solution {
boolean[][] f; // f[i][j]:s[i..j] 是否是回文串
List<List<String>> ret = new ArrayList<>(); // 最终结果
List<String> ans = new ArrayList<>(); // 当前分割方案
int n; // 字符串长度
public List<List<String>> partition(String s) {
n = s.length();
f = new boolean[n][n];
// 初始化:所有单个字符都是回文
for (int i = 0; i < n; ++i) {
Arrays.fill(f[i], true);
}
// DP 预处理:快速判断任意子串是不是回文
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
dfs(s, 0); // 回溯搜索所有分割方案
return ret;
}
// 回溯:从位置 i 开始分割
void dfs(String s, int i) {
if (i == n) { // 分割到末尾,记录方案
ret.add(new ArrayList<>(ans));
return;
}
// 尝试所有可能的结束位置 j
for (int j = i; j < n; ++j) {
if (f[i][j]) { // 如果 s[i..j] 是回文
ans.add(s.substring(i, j + 1)); // 选它
dfs(s, j + 1); // 继续分割后面
ans.remove(ans.size() - 1); // 回溯撤销
}
}
}
}
3.例子
以 **s = "aab"**为例
输入
s = "aab"
字符下标:0:a,1:a,2:b
n = 3
第一步:初始化 DP 数组 f
f = new boolean[3][3];
执行:
for (int i = 0; i < 3; i++)
Arrays.fill(f[i], true);
结果:
f[0][0] = true f[0][1] = true f[0][2] = true
f[1][0] = true f[1][1] = true f[1][2] = true
f[2][0] = true f[2][1] = true f[2][2] = true
第二步:DP 预处理(i 倒序,j 正序)
for (i = 2; i >= 0; i--)
for (j = i+1; j < 3; j++)
f[i][j] = (s[i]==s[j]) && f[i+1][j-1];
运行过程:
i=2
j 从 3 开始,无循环
i=1
j=2
s[1] = a
s[2] = b
不相等
f[1][2] = false
i=0
j=1
s[0]=a,s[1]=a 相等
f[1][0] 为 true
所以 f[0][1] = true
j=2
s[0]=a,s[2]=b 不相等
f[0][2] = false
最终 f 数组:
f[0][0] = true
f[0][1] = true
f[0][2] = false
f[1][1] = true
f[1][2] = false
f[2][2] = true
第三步:DFS 回溯搜索分割方案
从 i=0 开始
第一层:i=0
遍历 j=0、1、2
j=0
f[0][0] = true
把 s[0:0] = "a" 加入 ans
ans = ["a"]
递归进入 i=1
第二层:i=1
遍历 j=1、2
j=1
f[1][1] = true
加入 s[1:1] = "a"
ans = ["a","a"]
递归进入 i=2
第三层:i=2
遍历 j=2
j=2
f[2][2] = true
加入 "b"
ans = ["a","a","b"]
递归进入 i=3
i == n → 把 ans 加入结果
得到第一个方案:["a","a","b"]
回溯:
删除 "b"
回到 i=2,结束
删除 "a"
回到 i=1
回到 i=1,j=2
f[1][2] = false → 跳过
回溯到 i=0
删除 "a"
ans = []
继续 j=1
j=1
f[0][1] = true
加入 s[0:1] = "aa"
ans = ["aa"]
递归进入 i=2
进入 i=2
j=2
f[2][2] = true
加入 "b"
ans = ["aa","b"]
i=3 → 加入结果
得到第二个方案:["aa","b"]
最终结果
[["a","a","b"],["aa","b"]]
总结
- DP 预处理:提前算出所有区间是不是回文
- DFS:从 i 开始,尝试所有能切的 j
- 回溯:加入 → 递归 → 删除,尝试所有可能
- 最终得到所有合法分割方案
四、红皇后
1.题目
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 1:

输入:n = 4
输出:[[".Q...","...Q","Q...","...Q."],["...Q.","Q...","...Q",".Q..."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
提示:
- 1 <= n <= 9
2.代码
java
class Solution {
// 存储所有合法的N皇后摆放方案结果集
private List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 初始化n×n的字符数组棋盘,char数组默认值为'\u0000'(空字符)
char[][] board = new char[n][n];
// 遍历每一行,将整行填充为'.'(表示空位)
for (char[] row : board) {
Arrays.fill(row, '.'); // 利用Arrays.fill快速填充数组元素
}
// 从第0行开始回溯,尝试摆放皇后
backtrack(board, 0);
return res;
}
private void backtrack(char[][] board, int row) {
int n = board.length; // 棋盘的边长(皇后数量)
// 终止条件:当处理完所有行(row == n),说明找到一个合法方案
if (row == n) {
// 将字符数组表示的棋盘转换为List<String>格式,存入结果集
List<String> validBoard = new ArrayList<>();
for (char[] r : board) {
validBoard.add(new String(r)); // 字符数组转字符串
}
res.add(validBoard); // 加入结果集
return; // 结束当前递归分支,返回上一层
}
// 遍历当前行的每一列(0~n-1),尝试在该列摆放皇后
for (int col = 0; col < n; col++) {
// 检查当前位置(row, col)是否可以安全摆放皇后(无冲突)
if (isValid(board, row, col)) {
// 做出选择:在(row, col)位置放置皇后(标记为'Q')
board[row][col] = 'Q';
// 递归处理下一行的皇后摆放
backtrack(board, row + 1);
// 回溯:撤销选择,将(row, col)位置恢复为'.'(空位)
board[row][col] = '.';
}
}
}
//辅助方法:检查指定位置(row, col)是否可以安全摆放皇后
private boolean isValid(char[][] board, int row, int col) {
int n = board.length; // 棋盘边长
// 1. 检查当前列是否已有皇后(遍历当前行上方的所有行)
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') {
return false; // 同一列存在皇后,冲突
}
}
// 2. 检查左上方对角线(行号递减,列号递减,直到边界)
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') {
return false; // 冲突
}
}
// 3. 检查右上方对角线(行号递减,列号递增,直到边界)
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q') {
return false; // 冲突
}
}
// 所有攻击方向均无皇后,位置安全
return true;
}
}
3.例子
下面用n=4为例子
初始状态
n=4,棋盘初始化为4×4,全部为.
. . . .
. . . .
. . . .
. . . .
调用 backtrack(board, 0)
第1层递归:row = 0
遍历列 0、1、2、3
尝试 col = 0
isValid 检查通过,放置 Q
Q . . .
. . . .
. . . .
. . . .
进入下一层:backtrack(board, 1)
第2层递归:row = 1
遍历列 0、1、2、3
-
col=0:同列有 Q,不合法
-
col=1:对角线冲突,不合法
-
col=2:合法,放置 Q
Q . . .
. . Q .
. . . .
. . . .
进入下一层:backtrack(board, 2)
第3层递归:row = 2
遍历所有列
所有列均被攻击,无合法位置
回溯,撤销 row=1,col=2 的 Q
回到 row=1,继续尝试下一列
尝试 col=3
合法,放置 Q
Q . . .
. . . Q
. . . .
. . . .
进入下一层:backtrack(board, 2)
第3层递归:row = 2
遍历列
-
col=0:冲突
-
col=1:合法,放置 Q
Q . . .
. . . Q
. Q . .
. . . .
进入下一层:backtrack(board, 3)
第4层递归:row = 3
遍历所有列
均被攻击,无合法位置
逐层回溯,撤销所有 Q,回到 row=0
回到 row=0,尝试 col=1
放置 Q
. Q . .
. . . .
. . . .
. . . .
进入 row=1
row=1 遍历列
找到合法位置 col=3,放置 Q
. Q . .
. . . Q
. . . .
. . . .
进入 row=2
row=2 遍历列
找到合法位置 col=0,放置 Q
. Q . .
. . . Q
Q . . .
. . . .
进入 row=3
row=3 遍历列
找到合法位置 col=2,放置 Q
. Q . .
. . . Q
Q . . .
. . Q .
进入下一层:backtrack(board, 4)
此时 row == n,满足终止条件
将当前棋盘加入结果集,得到第一个解。
继续回溯、尝试
代码会继续撤销、尝试其他列,最终找到第二个解:
. . Q .
Q . . .
. . . Q
. Q . .
最终结果
4皇后问题共有两组解,代码执行结束后返回这两个方案。
总结
好啦,到现在这篇文章为止,hot100系列已经正式结束了,感谢大家的支持呀!如果对您有帮助,可以点赞收藏评论哦!也可以关注主包,主包以后也会分享其他方面的知识呀!!!