回溯
# 回溯法和DFS什么关系
def dfs(node, visited):
if node in visited: # 仅访问未遍历节点
return
visited.add(node)
for neighbor in node.neighbors:
dfs(neighbor, visited) # 无状态重置操作
def backtrack(path, used, nums, res):
if len(path) == len(nums): # 终止条件:找到解
res.append(path.copy())
return
for i in range(len(nums)):
if used[i]: continue # 剪枝:跳过已用元素
used[i] = True # 记录状态
path.append(nums[i])
backtrack(path, used, nums, res) # 递归探索
path.pop() # 回溯:撤销选择
used[i] = False # 回溯:重置状态
所有回溯法都是DFS的应用,但并非所有DFS都需回溯机制。实际应用中,回溯法的“选择→递归→撤销”模板(见)是解决组合优化问题的核心范式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 22. 括号生成 - 力扣(LeetCode) (opens new window)
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
backtrack(res, new StringBuilder(), 0, 0, n);
return res;
}
/**
List<String> res 存储所有合法的括号组合结果集
StringBuilder cur 动态记录当前递归路径下已生成的括号序列
int open 当前路径中已使用的左括号数量
int close 当前路径中已使用的右括号数量
int max 目标括号总对数(如生成 n=3 对括号时,max=3)
*/
private void backtrack(List<String> res, StringBuilder cur, int open, int close, int max) {
// 收集结果
if (cur.length() == 2 * max) {
res.add(cur.toString());
return;
}
if (open < max) {
cur.append("(");
backtrack(res, cur, open + 1, close, max);
cur.deleteCharAt(cur.length() - 1); // 回溯
}
if (close < open) {
cur.append(")");
backtrack(res, cur, open, close + 1, max);
cur.deleteCharAt(cur.length() - 1); // 回溯
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 93. 复原 IP 地址 - 力扣(LeetCode) (opens new window)
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<>();
backtrack(s, 0, new ArrayList<>(), res);
return res;
}
/**
s 原字符串
begin 将要分割的位置(begin, begin + len)为下一选择要分割的字符串
path 记录当前递归路径下已做出的选择序列
res 结果
*/
private void backtrack(String s, int begin, List<String> path, List<String> res) {
// 终止条件:4段且用尽字符
if (path.size() == 4) { // 为4段
if (begin == s.length()) // 并且字符刚好被分割完
res.add(String.join(".", path));
return;
}
// 剪枝1:剩余字符数不合法
int remainSegments = 4 - path.size();
if (s.length() - begin > remainSegments * 3 || s.length() - begin < remainSegments) return;
// 尝试截取1-3位
for (int len = 1; len <= 3 && begin + len <= s.length(); len++) {
String segment = s.substring(begin, begin + len);
// 剪枝2:前导零或数值超范围
if ((segment.startsWith("0") && segment.length() > 1) || Integer.parseInt(segment) > 255)
continue;
path.add(segment); // 选择当前段
backtrack(s, begin + len, path, res); // 递归后续
path.remove(path.size() - 1); // 撤销选择
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 51. N 皇后 - 力扣(LeetCode) (opens new window)
class Solution {
private List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 每个字符串代表一行,字符串列表代表一个棋盘
// '.' 表示空,'Q' 表示皇后,初始化空棋盘
List<String> board = new ArrayList<>();
for (int i = 0;i < n; i++) {
board.add(".".repeat(n));
}
backtrack(board, 0);
return res;
}
// 结束条件:row 超过 board 的最后一行
private void backtrack(List<String> board, int row) {
// 结束条件:row 超过 board 的最后一行
if (row == board.size()) {
res.add(new ArrayList<>(board));
return;
}
int n = board.get(row).length();
for (int col = 0; col < n; col++) {
if (!isValid(board,row,col)) {
continue;
}
// 做选择
char[] rowChars = board.get(row).toCharArray();
rowChars[col] = 'Q';
board.set(row, new String(rowChars));
// 进入下一行决策
backtrack(board, row + 1);
// 撤销选择
rowChars[col] = '.';
board.set(row, new String(rowChars));
}
}
// 是否可以在 board[row][col] 放置皇后?
private boolean isValid(List<String> board, int row, int col) {
int n = board.size();
// 检查列是否有皇后互相冲突
for (int i =0; i <= row; i++) {
if (board.get(i).charAt(col) == 'Q') {
return false;
}
}
// 检查右上方是否有皇后互相冲突
for (int i = row - 1,j = col + 1; i >= 0 && j < n; i--,j++) {
if (board.get(i).charAt(j) == 'Q') {
return false;
}
}
// 检查左上方是否有皇后互相冲突
for (int i = row - 1,j = col - 1; i >= 0 && j >= 0; i--,j--) {
if (board.get(i).charAt(j) == 'Q') {
return false;
}
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 元素无重不可复选
# 78. 子集 - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new LinkedList<>(track));
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
//做选择
track.addLast(nums[i]);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(nums,i + 1);
// 撤销选择
track.removeLast();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 77. 组合 - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtrack(1, n, k);
return res;
}
void backtrack(int start, int n, int k) {
// 前序位置,每个节点的值都是一个子集
if (k == track.size()) {
//遍历到了第 k 层,收集当前节点的值
res.add(new LinkedList<>(track));
return;
}
// 回溯算法标准框架
for (int i = start; i <= n; i++) {
//做选择
track.addLast(i);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(i + 1, n, k);
// 撤销选择
track.removeLast();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 46. 全排列 - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// track 中的元素会被标记为 true
boolean[] used;
// 主函数,输入一组不重复的数字,返回它们的全排列
public List<List<Integer>> permute(int[] nums) {
used = new boolean[nums.length];
backtrack(nums);
return res;
}
// 回溯算法核心函数
void backtrack(int[] nums) {
// base case,到达叶子节点
if (track.size() == nums.length) {
// 收集叶子节点上的值
res.add(new LinkedList(track));
return;
}
// 回溯算法标准框架
for (int i = 0; i < nums.length; i++) {
// 已经存在 track 中的元素,不能重复选择
if (used[i]) {
continue;
}
// 做选择
used[i] = true;
track.addLast(nums[i]);
// 进入下一层回溯树
backtrack(nums);
// 取消选择
track.removeLast();
used[i] = false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 元素可重不可复选(排序+剪枝)
# 90. 子集 II - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 先排序,让相同的元素靠在一起
Arrays.sort(nums);
backtrack(nums, 0);
return res;
}
void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new LinkedList<>(track));
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 剪枝逻辑,值相同的相邻树枝,只遍历第一条
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
//做选择
track.addLast(nums[i]);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(nums,i + 1);
// 撤销选择
track.removeLast();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 40. 组合总和 II - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// 记录 track 中的元素之和
int trackSum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if (candidates.length == 0) {
return res;
}
// 先排序,让相同的元素靠在一起
Arrays.sort(candidates);
backtrack(candidates, 0, target);
return res;
}
void backtrack(int[] nums, int start, int target) {
// base case,达到目标和,找到符合条件的组合
if (trackSum == target) {
res.add(new LinkedList<>(track));
return;
}
// base case,超过目标和,直接结束
if (trackSum > target) {
return;
}
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 剪枝逻辑,值相同的树枝,只遍历第一条
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
//做选择
track.addLast(nums[i]);
trackSum += nums[i];
// 递归遍历下一层回溯树
backtrack(nums, i + 1, target);
// 撤销选择
track.removeLast();
trackSum -= nums[i];
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 47. 全排列 II - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// track 中的元素会被标记为 true
boolean[] used;
// 主函数,输入一组不重复的数字,返回它们的全排列
public List<List<Integer>> permuteUnique(int[] nums) {
// 先排序,让相同的元素靠在一起
Arrays.sort(nums);
used = new boolean[nums.length];
backtrack(nums);
return res;
}
// 回溯算法核心函数
void backtrack(int[] nums) {
// base case,到达叶子节点
if (track.size() == nums.length) {
// 收集叶子节点上的值
res.add(new LinkedList(track));
return;
}
// 回溯算法标准框架
for (int i = 0; i < nums.length; i++) {
// 已经存在 track 中的元素,不能重复选择
if (used[i]) {
continue;
}
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
// 做选择
used[i] = true;
track.addLast(nums[i]);
// 进入下一层回溯树
backtrack(nums);
// 取消选择
track.removeLast();
used[i] = false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 元素无重可复选( i + 1 ---> i )
# 39. 组合总和 - 力扣(LeetCode) (opens new window)
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// 记录 track 中的元素之和
int trackSum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates.length == 0) {
return res;
}
backtrack(candidates, 0, target);
return res;
}
void backtrack(int[] nums, int start, int target) {
// base case,达到目标和,找到符合条件的组合
if (trackSum == target) {
res.add(new LinkedList<>(track));
return;
}
// base case,超过目标和,直接结束
if (trackSum > target) {
return;
}
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
//做选择
track.addLast(nums[i]);
trackSum += nums[i];
// 递归遍历下一层回溯树
backtrack(nums, i , target);// 同一元素可重复使用,注意参数 i+1 ---> i
// 撤销选择
track.removeLast();
trackSum -= nums[i];
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
编辑 (opens new window)
上次更新: 2025/07/16, 10:21:39