这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情
题目
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入: n = 1
输出: [["Q"]]
提示:
皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 )
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/n-…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
一个皇后位置确定后,其他皇后的可能摆放位置会发生变化,思路就是要判断出合法位置与非法位置,那么如何识别合法与非法位置呢?核心就是根据横、纵、斜三种攻击方式,建立四个数组,分别存储哪些行、列、撇、捺位置是不能放置的,然后将所有合法位置都作为下一次递归的可能位置,直到皇后放完,或者无位置可放为止。
当前一行已经落下一个皇后之后,下一行需要判断三个条件:
- 一列上不能存在其他皇后
- 对角线【左上-右下】不能存在其他皇后
- 对角线【右上-左下】不能存在其他皇后
其中难点是对角线上是否存在皇后的判断:
直接通过这个点的横纵坐标 rowIndex + columnIndex 相加,相等的话就在同在对角线 1 上。
直接通过这个点的横纵坐标 rowIndex - columnIndex 相减,相等的话就在同在对角线 2 上:
步骤:
- 用
columns数组记录摆放过的列下标,摆放过后直接标记为 true 即可。 - 用
dia1数组记录摆放过的对角线1下标,摆放过后直接把下标rowIndex + columnIndex标记为 true 即可。 - 用
dia2数组记录摆放过的对角线1下标,摆放过后直接把下标rowIndex - columnIndex标记为 true 即可。 - 递归函数的参数
row代表每一行中皇后放置的列数,比如row[0] = 3代表第 0 行皇后放在第 3 列,以此类推。 - 每次进入递归函数前,先把当前项所对应的列、对角线1、对角线2的下标标记为 true,带着标记后的状态进入递归函数。并且在退出本次递归后,需要把这些状态重置为 false ,再进入下一轮循环。
有了这几个辅助知识点,就可以开始编写递归函数了,在每一行,我们都不断的尝试一个坐标点,只要它和之前已有的结果都不冲突,那么就可以放入数组中作为下一次递归的开始值。
这样,如果递归函数顺利的来到了 rowIndex === n 的情况,说明之前的条件全部满足了,一个 n皇后 的解就产生了。
代码
let solveNQueens = function (n) {
let res = []
// 已摆放皇后的的列下标
let columns = []
// 已摆放皇后的对角线1下标 左下 -> 右上
// 计算某个坐标是否在这个对角线的方式是「行下标 + 列下标」是否相等
let dia1 = []
// 已摆放皇后的对角线2下标 左上 -> 右下
// 计算某个坐标是否在这个对角线的方式是「行下标 - 列下标」是否相等
let dia2 = []
// 尝试在一个n皇后问题中 摆放第index行内的皇后位置
let putQueen = (rowIndex, row) => {
if (rowIndex === n) {
res.push(generateBoard(row))
return
}
// 尝试摆第index行的皇后 尝试[0, n-1]列
for (let columnIndex = 0; columnIndex < n; columnIndex++) {
// 在列上不冲突
let columnNotConflict = !columns[columnIndex]
// 在对角线1上不冲突
let dia1NotConflict = !dia1[rowIndex + columnIndex]
// 在对角线2上不冲突
let dia2NotConflict = !dia2[rowIndex - columnIndex]
if (columnNotConflict && dia1NotConflict && dia2NotConflict) {
columns[columnIndex] = true
dia1[rowIndex + columnIndex] = true
dia2[rowIndex - columnIndex] = true
putQueen(rowIndex + 1, row.concat(columnIndex))
columns[columnIndex] = false
dia1[rowIndex + columnIndex] = false
dia2[rowIndex - columnIndex] = false
}
}
}
putQueen(0, [])
return res
}
function generateBoard(row) {
let n = row.length
let res = []
for (let y = 0; y < n; y++) {
let cur = ""
for (let x = 0; x < n; x++) {
if (x === row[y]) {
cur += "Q"
} else {
cur += "."
}
}
res.push(cur)
}
return res
}
总结
从这道题目可以总结以下,回溯算法的思想,形象一点可以将‘回溯’比喻成走迷宫,遇到障碍物就从头 “回溯” 继续探索。
递归解决回溯的套路:
function func(params: any[], results: any[] = []) {
// 消耗 params 生成 currentResult
const { currentResult, restParams } = doSomething(params);
// 如果 params 还有剩余,则递归消耗,直到 params 耗尽为止
if (restParams.length > 0) func(restParams, results.concat(currentResult));
}
参考文章: