题目描述:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
解题思路:
通过递归和回溯的方式,尝试所有可能的皇后放置方法,直到找到所有合法的解决方案。
以下是解决N皇后问题的基本步骤和思路:
-
问题定义:
在N×N的棋盘上放置N个皇后,使得它们互不攻击,即任何两个皇后都不在同一行、同一列或同一对角线上。 -
初始化棋盘:
创建一个N×N的二维数组board
,用.
表示空位,用'Q'
表示皇后的位置,初始时所有位置都是空的。 -
定义合法性检查函数(
isValid
):
编写一个函数,检查在棋盘的(row, col)
位置放置皇后是否会导致攻击冲突。这通常通过检查:- 当前列是否已有皇后。
- 左上对角线(
i - j
)和右上对角线(i + j
)是否有皇后。
-
递归放置函数(
helper
):
编写一个递归函数,从棋盘的第一行开始尝试放置皇后:- 如果当前行数等于N,表示所有皇后都已放置完毕,记录当前棋盘状态为一个解决方案。
- 对当前行的每一列调用
isValid
函数检查是否可以放置皇后。 - 如果可以放置,将皇后放在该位置,然后递归调用
helper
函数尝试放置下一行的皇后。 - 如果下一行没有找到合法位置放置皇后,或者已经放置完所有皇后但不是有效的解决方案,递归返回并进行回溯。
-
回溯:
当helper
函数在某一位置找不到合法的皇后放置位置时,它会回溯到上一行,将之前放置的皇后移除(用.
替换'Q'
),然后尝试该行的其他列位置。 -
收集解决方案:
每当找到一个合法的棋盘布局时,就将其添加到解决方案数组res
中。 -
开始递归:
从棋盘的第一行(通常是0行)开始调用helper
函数,尝试放置第一个皇后。 -
返回所有解决方案:
当所有可能的放置方式都被探索后,函数返回包含所有解决方案的数组。 -
输出或使用解决方案:
根据需要,可以打印或以其他方式使用找到的所有解决方案。
这个算法的关键在于递归和回溯的结合使用,它允许程序系统地探索所有可能的解决方案,同时在遇到当前路径不可能形成解决方案时及时回溯,避免无效搜索。
代码
/**
* @param {number} n
* @return {string[][]}
*/
var solveNQueens = function (n) {
// 初始化棋盘为n×n的二维数组,用'.'表示空位
const board = new Array(n);
for (let i = 0; i < n; i++) {
board[i] = new Array(n).fill('.'); // 每一行都是n个'.'字符的数组
}
// 初始化一个数组用来存放所有解决方案
const res = [];
// 定义一个函数来检查在给定位置(row, col)放置皇后是否合法
const isValid = (row, col) => {
// 检查之前的行是否有皇后在同一列或对角线上
for (let i = 0; i < row; i++) {
for (let j = 0; j < n; j++) {
// 如果之前行的某个位置是皇后,并且与当前位置在同一列或对角线上
if (board[i][j] === 'Q' &&
(j === col || // 同一列
i - j === row - col || // 左上对角线
i + j === row + col)) { // 右上对角线
return false; // 放置不合法
}
}
}
return true; // 放置合法
};
// 定义一个递归函数来尝试放置皇后
const helper = (row) => {
// 如果当前行数等于n,说明已经放置完所有皇后,找到一个解决方案
if (row === n) {
// 将当前棋盘状态复制并保存为解决方案
res.push(board.map(row => row.join('')));
return;
}
// 尝试在当前行的每个位置放置皇后
for (let col = 0; col < n; col++) {
// 如果放置皇后的位置是合法的
if (isValid(row, col)) {
// 在当前位置放置皇后
board[row][col] = "Q";
// 递归放置下一行的皇后
helper(row + 1);
// 回溯,移除当前位置的皇后,尝试其他位置
board[row][col] = '.';
}
}
};
// 从棋盘的第一行开始放置皇后
helper(0);
// 返回所有找到的解决方案
return res;
};
代码注解
board[i] = new Array(4).fill('.')
这行代码创建了一个长度为4的一维数组,并且使用 .fill('.')
方法将这个数组的每个元素都设置为字符串 '.'
。这个数组代表棋盘的一行。
所以,如果我们逐行看这个数组,它的形状是这样的:
[
[ '.', '.', '.', '.' ], // 第0行
[ '.', '.', '.', '.' ], // 第1行
[ '.', '.', '.', '.' ], // 第2行
[ '.', '.', '.', '.' ], // 第3行
]
这里,每个子数组 [ '.', '.', '.', '.' ]
都是 board
数组的一个元素,代表棋盘上的一行,每个元素 '.'
代表棋盘上的一个空位。整个 board
数组是一个二维数组,有4行4列,总共16个元素。
提示:
在N皇后问题的递归回溯算法中,如果在最后一行没有找到放置皇后的合法位置,递归调用会正常返回,但不会向解决方案数组
res
添加任何内容。这是因为只有在成功放置了所有N
个皇后,并且所有皇后都不相互攻击时,才会记录解决方案。
这里是递归调用的逻辑流程:
- 递归调用
helper(row + 1)
尝试在下一行放置皇后。 - 如果在当前行(
row
)成功放置了一个皇后,递归进入下一行。 - 在下一行,继续尝试放置皇后,重复步骤1和2。
- 如果在任何行找不到合法的皇后位置,或者违反了皇后放置规则,递归调用会结束,并开始回溯。
- 在回溯过程中,
helper(row + 1)
返回到上一次递归调用的状态,此时会执行board[row][col] = '.';
来撤销当前行col
列的皇后放置。 - 外层循环(
for (let col = 0; col < n; col++)
)会继续尝试当前行的下一个列位置。
如果在最后一行(row + 1 == n
)没有找到合法的皇后位置,递归调用会一直回溯到更早的行,直到找到可以放置皇后的位置,或者确定当前部分解决方案不可行。在后一种情况下,递归会一直回溯到最开始,不会产生任何解决方案。
重要的是,递归调用本身不会返回任何“值”,它通过修改全局状态(即棋盘 board
)来进行工作。解决方案是通过检查棋盘状态来收集的,只有当所有N个皇后都合法放置时,才会将棋盘状态添加到解决方案数组 res
中。如果递归到达最后一行但没有找到解决方案,res
数组不会受到影响,仍然保持之前的状态。