引言

回溯算法是一种通过探索所有可能解来找到所有解或最优解的算法。本文系统讲解回溯算法的核心思想和经典问题。

一、回溯算法基础

1.1 回溯算法模板

1
2
3
4
5
6
7
8
9
10
11
12
void backtrack(路径, 选择列表) {
if (满足结束条件) {
result.add(路径);
return;
}

for (选择 : 选择列表) {
做选择;
backtrack(路径, 选择列表);
撤销选择;
}
}

二、经典回溯问题

2.1 全排列(LeetCode 46)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, new boolean[nums.length]);
return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path,
int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}

for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;

path.add(nums[i]);
used[i] = true;
backtrack(result, path, nums, used);
used[i] = false;
path.remove(path.size() - 1);
}
}

2.2 组合(LeetCode 77)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), 1, n, k);
return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path,
int start, int n, int k) {
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}

for (int i = start; i <= n; i++) {
path.add(i);
backtrack(result, path, i + 1, n, k);
path.remove(path.size() - 1);
}
}

2.3 子集(LeetCode 78)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path,
int[] nums, int start) {
result.add(new ArrayList<>(path));

for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backtrack(result, path, nums, i + 1);
path.remove(path.size() - 1);
}
}

2.4 N皇后(LeetCode 51)

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
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
char[][] board = new char[n][n];
for (char[] row : board) {
Arrays.fill(row, '.');
}
backtrack(result, board, 0);
return result;
}

private void backtrack(List<List<String>> result, char[][] board, int row) {
if (row == board.length) {
result.add(construct(board));
return;
}

for (int col = 0; col < board.length; col++) {
if (isValid(board, row, col)) {
board[row][col] = 'Q';
backtrack(result, board, row + 1);
board[row][col] = '.';
}
}
}

private boolean isValid(char[][] board, int row, int col) {
// 检查列
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') return false;
}

// 检查左上对角线
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') return false;
}

// 检查右上对角线
for (int i = row - 1, j = col + 1; i >= 0 && j < board.length; i--, j++) {
if (board[i][j] == 'Q') return false;
}

return true;
}

private List<String> construct(char[][] board) {
List<String> result = new ArrayList<>();
for (char[] row : board) {
result.add(new String(row));
}
return result;
}

三、优化技巧

3.1 剪枝

1
2
3
4
// 在递归前判断是否需要继续
if (条件不满足) {
return; // 剪枝
}

3.2 去重

1
2
3
4
5
// 对于有重复元素的问题
Arrays.sort(nums);
if (i > start && nums[i] == nums[i - 1]) {
continue; // 跳过重复元素
}

总结

回溯算法核心:

  1. 模板:选择、回溯、撤销
  2. 应用:排列、组合、子集
  3. 优化:剪枝、去重

参考资料