算法学习之回溯问题
解决一个回溯问题,实际上就是一个决策树的遍历过程
概念
在解决回溯问题时,我们需要关注:
- 路径:已经做出的选择
- 选择列表:当前情况下可以做出的选择
- 结束条件:到达决策树底层,无法再做选择的条件
算法框架(伪代码)
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
#### 做选择
# 将该选择从选择列表移除
选择列表.remove(选择)
# 把选择加入到路径中
路径.add(选择)
# 递归
backtrack(路径, 选择列表)
#### 撤销选择
# 把选择从路径中删除
路径.remove(选择)
#将该选择再加入选择列表
选择列表.add(选择)
这里我已经把for循环内的代码分为了三个部分,先做选择,然后在这个选择的基础下进行下一次的递归,最后一步就是撤销之前的选择。
注意:做选择与撤销选择的步骤是镜像的。
这张图中,[2]就是「路径」,表示已经做过的选择;[1,3]就是「选择列表」,表示可以做出的选择;「结束条件」就是遍历到树的底层,在这里就是选择列表为空的时候。
在做选择前,从选择列表中移除,然后路径中加入选择。递归。在路径中删除选择,在选择列表中添加选择。
例题
全排列问题
输入一个数组,给出这个数组的所有排列方式。
例如输入:[1,2],输出:[[1,2],[2,1]]
List<List<Integer>> res = new LinkedList<>();
List<List<Integer>> permute(int[] nums) {
// 记录路径
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
void backtrack(int[] nums, LinkedList<Integer> track) {
// 结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
// 这个代替了从选择队列中删除选择,与递归后的恢复选择
if (track.contains(nums[i]))
continue;
// 做出选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 撤销选择
track.removeLast();
}
}
N皇后问题
给你一个 N×N 的棋盘,让你放置 N 个 皇后,使得它们不能互相攻击。
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
思路:可以把棋盘的每一行想成是决策树的一层,而这一行中的每一列就是决策树这一层中的N个节点。
List<char[][]> res = new ArrayList<>();
List<char[][]> solveNQueens(int n) {
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
backtrack(board, 0);
return res;
}
void backtrack(char[][] board, int row) {
if (row == board.length) {
int n = board.length;
char[][] tmp = new char[n][n];
// 注意深浅拷贝问题。clone方法在二维数组时是浅拷贝。
// 解决方法就是一行一行的使用clone
for (int i = 0; i < n; i++) {
tmp[i] = board[i].clone();
}
res.add(tmp);
return;
}
int n = board.length;
for (int col = 0; col < n; col++) {
if (!isValid(board, row, col)) {
continue;
}
board[row][col] = 'Q';
backtrack(board, row + 1);
board[row][col] = '.';
}
}
boolean isValid(char[][] board, int row, int col) {
int n = board.length;
// 左右上下
for (int i = 0; i < n; i++) {
if (board[i][col] == 'Q' || board[row][i] == 'Q')
return false;
}
// 右上
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q')
return false;
}
// 右下
for (int i = row + 1, j = col + 1; i < n && j < n; i++, j++) {
if (board[i][j] == '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 < n && j >= 0; i++, j--) {
if (board[i][j] == 'Q')
return false;
}
return true;
}
个人总结
思路:for(放在首位的多个选择){从数组中取元素,递归,把取的元素撤销。}
之前要把可做的选择给放到数组中。
注意:深浅拷贝问题
申明:本博文是看了labuladong的算法小抄之后个人的理解以及总结。