332. 重新安排行程
直觉上来看这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。
这道题算是图论里深搜的题目,可以用回溯法的套路来解这道题目,算是拓展一下思路,原来回溯法还可以这么玩!
难度较高,之后再来解决。
51. N皇后
首先来看一下皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
思路:搜索皇后的位置,可以抽象为一棵树。
下面用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么用皇后们的约束条件,回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
class Solution {
private List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// '.' 表示空,'Q' 表示皇后,初始化空棋盘。
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backtracking(n, 0, chessboard);
return res;
}
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行
private void backtracking(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0; col < n; col++) {
// 排除不合法选择
if (!isValid(row, col, n, chessboard)) {
continue;
}
// 做选择
chessboard[row][col] = 'Q';
// 进入下一行决策
backtracking(n, row + 1, chessboard);
// 撤销选择
chessboard[row][col] = '.';
}
}
// 二维数组转换为List
private List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.valueOf(c));
}
return list;
}
// 检查是否可以在 chessboard[row][col] 放置皇后
private boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查同列
for (int i = 0; i < row; i++) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线(即左上方)
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线(即右上方)
for (int i = row - 1, j = col + 1; i >= 0 && j <= n - 1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
为什么没有对同一行进行检查呢?
因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。
37. 解数独
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3 x 3 宫格内只能出现一次。
空白格用 ‘.’ 表示。
本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
思路:将问题抽象成树形结构,这个树形结构太大了,抽取一部分,如图所示:
在树形图中可以看出需要的是一个二维的递归(也就是两个for循环嵌套着递归)
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
class Solution {
public void solveSudoku(char[][] board) {
backtracking(board);
}
private boolean backtracking(char[][] board) {
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < 9; i++) { // 遍历行
for (int j = 0; j < 9; j++) { // 遍历列
if (board[i][j] != '.') { // 跳过已填入的数字
continue;
}
for (char k = '1'; k <= '9'; k++) {
// 判断 board[i][j] 这个位置放 k 是否合适
if (isValid(i, j, k, board)) {
board[i][j] = k;
if (backtracking(board)) {
return true; // 如果找到合适一组解立刻返回
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,
// 说明这个棋盘找不到解决数独问题的解,那么会直接返回。
// 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
// 判断 board[i][j] 是否可以填入 val
private boolean isValid(int row, int col, char val, char[][] board) {
// 同行是否重复
for (int i = 0; i < 9; i++) {
if (board[row][i] == val) {
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++) {
if (board[j][col] == val) {
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++) {
for (int j = startCol; j < startCol + 3; j++) {
if (board[i][j] == val) {
return false;
}
}
}
return true;
}
}
判断 board[
i][
j]
是否可以填入 val 的精简写法
boolean isValid(int r, int c, char n, char[][] board) {
for (int i = 0; i < 9; i++) {
// 判断行是否存在重复
if (board[r][i] == n) return false;
// 判断列是否存在重复
if (board[i][c] == n) return false;
// 判断 3 x 3 方框是否存在重复
if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
return false;
}
return true;
}
回溯法总结
回溯法就是一种暴力搜索的方法,重点在于将问题抽象成树形结构,和剪枝。只要出现了递归,就一定会有回溯。
回溯法可以解决以下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
回溯法主要就是利用递归来搜索问题的解,回溯法的模板如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
使用回溯法解决问题不要忘记递归三部曲(1、确定返回值,参数。2、确定终止条件。3、确定单层逻辑。 )