设计一种算法,打印 N 皇后在 N × N 棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/eight-queens-lcci
// 输入:4// 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]// 解释:4 // 皇后问题存在如下两个不同的解法。[[".Q..", // 解法 1"...Q","Q...","..Q."],["..Q.", // 解法 2"Q...","...Q",".Q.."]]
理解题意
把n个皇后放置在n*n的棋盘上,这些皇后彼此不能相互攻击。任何两个皇后都不能在同一行、同一列以及同一条斜线上。
数据结构及算法思维的选择
基本解法及编码的实现
相当于把每一行都会放置一个皇后,然后过滤掉不满足的结果,把正确的结果保存下来了。
暴力解法:
function solveNQueens(n) {// 1. 先实例一个二维数组// 声明一个输出结果的数组const result = [];/*** @param {result} 结果数组跟着传,存在满足要求的结果就push* @param {chess} 皇后的棋盘* @param {row} 当前行数*/solve(result, generator2DArray(n), 0);return result;};function solve(result, chess, row) {// 存在结果的边界条件,就是当现在row + 1的值等于chess的长度及满足if (row === chess.length) {// 需要把二维数组转为一维数组result.push(array2dTo1(chess));return;};// 遍历每一列for (let col = 0; col < chess.length; col++) {// 判断当前列是否满足放入皇后if (valid(row, col, chess)) {// 这里需要缓存满足情况的结果// 这样就算知道下面的结论不满足的时候,继续往上往下遍历,都是独立的棋盘。let tmp = copyArray2d(chess);tmp[row][col] = 'Q';// 进入下一行查找solve(result, tmp, row + 1);}}}function array2dTo1(array2D) {const res = [];for (const key in array2D) {res[key] = array2D[key].join('');};return res;}function generator2DArray(n) {const array2D = [];for (let i = 0; i < n; i++) {array2D[i] = [];for (let j = 0; j < n; j++) {array2D[i][j] = '.'}};return array2D;}function copyArray2d(array2D) {const newArr = [];for (const item of array2D) {newArr.push([...item])};return newArr;}function valid(row, col, chess) {// 下方的判断条件只考虑已经填入皇后的坐标// 判断这一列是否有存在的for (let i = 0; i < row; i++) {if (chess[i][col] === 'Q') {return false;}};// 判断左上角是否有存在的for (let i = row - 1, j = col - 1; i >= 0; i--, j--) {if (chess[i][j] === 'Q') {return false;}}// 判断右上角是否有存在for (let i = row - 1, j = col + 1; i >= 0; i--, j++) {if (chess[i][j] === 'Q') {return false;}};return true;}
时间复杂度:O(N!)
- N是皇后的数量
空间复杂度:O(N ^ 2)
边界细节问题和复杂度分析
function solveNQueens(n: number): string[][] {
const result = [], queues = []
// 左斜线
const left = new Set<number>();
// 右斜线
const right = new Set<number>();
// 列
const column = new Set<number>();
backTrack(result, queues, n, 0, left, right, column);
return result;
};
function backTrack(
result: string[][],
queues: number[],
n: number,
row: number,
left: Set<number>,
right: Set<number>,
column: Set<number>
) {
if (n === row) {
result.push(generatorBoard(queues, n));
return;
}
for (let col = 0; col < n; col++) {
let left1 = col - row;
// 如果左斜线上存在一个 即不满足
if (left.has(left1)) {
continue;
};
// 如果右斜线上存在一个 即不满足
let right1 = col + row;
if (right.has(right1)) {
continue;
};
// 如果列存在 即不满足
if (column.has(col)) {
continue;
};
// 当前满足 保存行和列信息
queues[row] = col;
// 保存满足行列信息的 左、右斜线和列的唯一值
left.add(left1), right.add(right1), column.add(col);
// 进入下一行
backTrack(result, queues, n, row + 1, left, right, column);
// 上述的满足不满足 删掉
queues[row] = -1;
// 清除掉不满足行列信息的 左、右斜线和列的唯一值
left.delete(left1), right.delete(right1), column.delete(col);
}
}
function generatorBoard(queues: number[], n): string[] {
const arr = [];
for (const index in queues) {
if (queues[index] === undefined) continue;
const row = (new Array(n)).fill('.');
row[queues[index]] = 'Q';
arr.push(row.join(''));
}
return arr;
}
时间复杂度:O(N!)
空间复杂度: O(N)
总结
- 回溯算法的特点
- 回溯是一种算法思想,可以用递归实现
- 回溯就是一种试探,类似于穷举,但回溯有剪枝功能
- 从问题的某一种可能出发,搜索从这种情况出发所能达到的所有可能,当一条路走到尽头的时候,再倒回出发点,从另一种可能出发,继续搜索。这就是回溯
- 回溯算法的应用
- 求和问题,给定七个数字,1234567求7的组合
- 从小到大搜索选择1+2+3+4那后面的 567就没必要重新组合了,这就是剪枝
