51. N 皇后
1.题目描述
- 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
- n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
- 给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
- 每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
2.解题思路
- 都知道n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二维矩阵还会有点不知所措。
- 首先来看一下皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
- 确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。
- 下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
- 从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
- 那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
3.代码实现
1.确定递归函数的参数和返回值:
递归函数定义:
- 定义全局变量二维数组res来记录最终结果。
- 参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。
List<List<String>> res = new ArrayList<>();
/**
* @param chessBoard
* @param n 为输入的棋盘大小
* @param row 是当前递归到棋盘的第几行了
*/
public void backTracking(char[][] chessBoard, int n, int row) {}
2.确定终止条件:
如下树形结构中可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。
代码如下:
//递归终止条件
//当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。
if (row == n) {
res.add(ArrayToList(chessBoard));
return;
}
3.确定单层递归的逻辑:
- 递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。
- 每次都是要从新的一行的起始位置开始搜,所以都是从0开始。
//单层递归逻辑
for (int col = 0; col < n; col++) {
if (isValid(row, col, n, chessBoard)) {// 验证合法就可以放
chessBoard[row][col] = 'Q';// 放置皇后
backTracking(chessBoard, n, row + 1);
chessBoard[row][col] = '.';// 回溯,撤销皇后
}
}
4.验证棋盘是否合法
- 按照如下标准去重:
- 不能同行
- 不能同列
- 不能同斜线 (45度和135度角)
代码如下:
//检测棋盘是否合法
public boolean isValid(int row, int col, int n, char[][] chessBoard) {
// 检查col列
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; i--, j++) {
if (chessBoard[i][j] == 'Q') {
return false;
}
}
return true;
}
完整代码如下:
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
//字符二维数组:棋盘n*n
char[][] chessBoard = new char[n][n];
for (char[] chars : chessBoard) {
Arrays.fill(chars, '.');
}
backTracking(chessBoard, n, 0);
return res;
}
/**
* @param chessBoard
* @param n 为输入的棋盘大小
* @param row 是当前递归到棋盘的第几行了
*/
public void backTracking(char[][] chessBoard, int n, int row) {
//当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。
if (row == n) {
res.add(ArrayToList(chessBoard));
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, n, chessBoard)) {// 验证合法就可以放
chessBoard[row][col] = 'Q';// 放置皇后
backTracking(chessBoard, n, row + 1);
chessBoard[row][col] = '.';// 回溯,撤销皇后
}
}
}
//将字符棋盘转为题目需要的List<String>
private List<String> ArrayToList(char[][] chessBoard) {
ArrayList<String> list = new ArrayList<>();
for (char[] c : chessBoard) {
list.add(String.copyValueOf(c));
}
return list;
}
//检测棋盘是否合法
public boolean isValid(int row, int col, int n, char[][] chessBoard) {
// 检查col列
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; i--, j++) {
if (chessBoard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
37. 解数独
1.题目描述
- 编写一个程序,通过填充空格来解决数独问题。
- 数独的解法需 遵循如下规则:
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
- 数独部分空格内已填入了数字,空白格用 '.' 表示。
2.解题思路
- N皇后问题是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。
- 本题就不一样了,本题中棋盘的每一个位置都要放一个数字(而N换后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
- 因为这个树形结构太大了,我抽取一部分,如图所示:
3.代码实现
1.确定递归函数的参数和返回值:
- 递归函数的返回值需要是bool类型,为什么呢?
- 因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
public boolean backTracking(char[][] board) {}
2.确定终止条件:
- 本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
- 不用终止条件会不会死循环?
- 递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
- 那么有没有永远填不满的情况呢?
- 这个问题我在递归单层搜索逻辑里在来讲!
3.确定单层递归的逻辑:
- 在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归)
- 一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
代码如下:(详细看注释)。
public boolean backTracking(char[][] board) {
// 一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < board.length; i++) {// 遍历行
for (int j = 0; j < board[0].length; j++) {// 遍历列
if (board[i][j] != '.') { // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++) {
if (isValid(i, j, k, board)) {// (i, j) 这个位置放k是否合适
board[i][j] = k; // 放置k
if (backTracking(board)) {
return true;// 如果找到合适一组立刻返回
}
board[i][j] = '.';// 回溯
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
- 注意这里return false的地方,这里放return false 是有讲究的。
- 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
- 那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!
4.判断棋盘是否合法
- 判断棋盘是否合法有如下三个维度:
- 同行是否重复
- 同列是否重复
- 9宫格里是否重复
- 代码如下:
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
public 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;
}
完整代码如下:
class Solution {
//37. 解数独
public void solveSudoku(char[][] board) {
backTracking(board);
}
public boolean backTracking(char[][] board) {
// 一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < board.length; i++) {// 遍历行
for (int j = 0; j < board[0].length; j++) {// 遍历列
if (board[i][j] != '.') { // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++) {
if (isValid(i, j, k, board)) {// (i, j) 这个位置放k是否合适
board[i][j] = k; // 放置k
if (backTracking(board)) {
return true;// 如果找到合适一组立刻返回
}
board[i][j] = '.';// 回溯
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
public 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;
}
}