LeetCode:N皇后-51

147 阅读4分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

题目

n  皇后问题研究的是如何将 n  个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

image.png

给定一个整数 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-…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

一个皇后位置确定后,其他皇后的可能摆放位置会发生变化,思路就是要判断出合法位置与非法位置,那么如何识别合法与非法位置呢?核心就是根据横、纵、斜三种攻击方式,建立四个数组,分别存储哪些行、列、撇、捺位置是不能放置的,然后将所有合法位置都作为下一次递归的可能位置,直到皇后放完,或者无位置可放为止。

当前一行已经落下一个皇后之后,下一行需要判断三个条件:

  1. 一列上不能存在其他皇后
  2. 对角线【左上-右下】不能存在其他皇后
  3. 对角线【右上-左下】不能存在其他皇后

其中难点是对角线上是否存在皇后的判断:

直接通过这个点的横纵坐标 rowIndex + columnIndex 相加,相等的话就在同在对角线 1 上。

image.png

直接通过这个点的横纵坐标 rowIndex - columnIndex 相减,相等的话就在同在对角线 2 上:

image.png

步骤:

  1. 用 columns 数组记录摆放过的下标,摆放过后直接标记为 true 即可。
  2. 用 dia1 数组记录摆放过的对角线1下标,摆放过后直接把下标 rowIndex + columnIndex标记为 true 即可。
  3. 用 dia2 数组记录摆放过的对角线1下标,摆放过后直接把下标 rowIndex - columnIndex标记为 true 即可。
  4. 递归函数的参数 row 代表每一行中皇后放置的列数,比如 row[0] = 3 代表第 0 行皇后放在第 3 列,以此类推。
  5. 每次进入递归函数前,先把当前项所对应的列、对角线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));
}

参考文章:

算法精读-回溯

n皇后问题