51. N 皇后

51. N 皇后

51. N 皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

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

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例 1:

在这里插入图片描述

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[["Q"]]

提示:

  • 1 <= n <= 9

思路

都知道n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二维矩阵还会有点不知所措。

首先来看一下皇后们的约束条件:

  • 不能同行
  • 不能同列
  • 不能同斜线

确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。

下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:

在这里插入图片描述

从图中,可以看出,二维矩阵中矩阵的高(行数)就是这棵树的高度,矩阵的宽(列数)就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。

回溯三部曲

按照我总结的如下回溯模板,我们来依次分析:

func backtracking(参数) {
    if 终止条件 {
        存放结果
        return
    }

    for 选择:本层集合中元素(树中节点孩子的数量就是集合的大小) {
        处理节点;
        backtracking(路径,下一层的选择列表) // 递归
        回溯,撤销处理结果
    }
}

1.递归函数参数
使用二维切片res来记录最终结果,使用n*n的二维切片arr模拟棋盘(初始时每个位置为'.'

然后用row来记录当前遍历到棋盘的第几行了。

代码如下:

func backtricking(res *[][]string,arr [][]byte,row int) {}

2.递归终止条件
在如下树形结构中:

在这里插入图片描述

可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。

代码如下:

// 棋盘的每一行转为string,整个棋盘就是[]string,然后放到res中([][]string)
if row == len(arr) {
    var temp []string
    for i := 0;i < len(arr);i++ {
        temp = append(temp,string(arr[i]))
    }
       
    *res = append(*res,temp)
    return 
}

3.单层搜索的逻辑
递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。

每次都是要从新的一行的起始位置开始搜,所以都是从0开始。

代码如下:

// 当前行的每个位置看看能不能放
for col := 0;col < len(arr[0]);col++{
    if !isValid(arr,row,col) { // 验证不合法就可以跳过
        continue
    }
    arr[row][col] = 'Q' // 放置皇后
    backtricking(res,arr,row+1)
    arr[row][col] = '.' // 回溯,撤销皇后
}

验证棋盘是否合法

按照如下标准去重:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线 (45度135度角)

代码如下:

func isValid(arr [][]byte,row,col int) bool {
    // 校验同一行其他位置都没有放过皇后,同一行是列在变
    for j := 0;j < len(arr);j++ {
        if j != col && arr[row][j] == 'Q' {
            return false
        }
    }

    // 校验同一列没有放过皇后,同一列是行在变,且只需要关心之前的行
    // 因为是从前面的行开始放的
    for i := 0;i < row;i++ {
        if arr[i][col] == 'Q' {
            return false
        }
    }

    // 校验同一斜线没有放过皇后 斜线:判断之前的行就可以
    // 假设当前坐标为i,j,则左上角坐标为i-1,j-1,右上角为i-1,j+1
    i,j := row - 1,col - 1
    for i >= 0 && j >= 0 {
        if arr[i][j] == 'Q' {
            return false
        }
        i--
        j--
    }

    i,j = row - 1,col + 1
    for i >= 0 && j <len(arr) {
        if arr[i][j] == 'Q' {
            return false
        }
        i--
        j++
    }
    
    return true
}

实际上同行可以不用检查,因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。

那么按照这个模板不难写出如下Go代码:

func solveNQueens(n int) [][]string {
    res := make([][]string,0)
    // 定义 n * n 的二维切片,每一轮在一行选择一个位置放
    arr := make([][]byte,n)
    for i:= 0;i < len(arr);i++ {
        arr[i] = make([]byte,n)
    }
    // 初始棋盘的每个位置都是'.'
    for i := 0;i < len(arr);i++ {
        for j := 0;j < len(arr[0]);j++{
            arr[i][j] = '.'
        }
    }

    backtricking(&res,arr,0)
    return res
}

func backtricking(res *[][]string,arr [][]byte,row int) {
    // 棋盘的每一行转为string,整个棋盘就是[]string,然后放到res中([][]string)
    if row == len(arr) {
        var temp []string
        for i := 0;i < len(arr);i++ {
            temp = append(temp,string(arr[i]))
        }
           
        *res = append(*res,temp)
        return 
    }

    // 当前行的每个位置看看能不能放
    for col := 0;col < len(arr[0]);col++{
        if !isValid(arr,row,col) {
            continue
        }
        arr[row][col] = 'Q'
        backtricking(res,arr,row+1)
        arr[row][col] = '.'
    }
    
}

func isValid(arr [][]byte,row,col int) bool {
    // 校验同一行其他位置都没有放过皇后,同一行是列在变
    for j := 0;j < len(arr);j++ {
        if j != col && arr[row][j] == 'Q' {
            return false
        }
    }

    // 校验同一列没有放过皇后,同一列是行在变,且只需要关心之前的行
    // 因为是从前面的行开始放的
    for i := 0;i < row;i++ {
        if arr[i][col] == 'Q' {
            return false
        }
    }

    // 校验同一斜线没有放过皇后 斜线:判断之前的行就可以
    // 假设当前坐标为i,j,则左上角坐标为i-1,j-1,右上角为i-1,j+1
    i,j := row - 1,col - 1
    for i >= 0 && j >= 0 {
        if arr[i][j] == 'Q' {
            return false
        }
        i--
        j--
    }

    i,j = row - 1,col + 1
    for i >= 0 && j <len(arr) {
        if arr[i][j] == 'Q' {
            return false
        }
        i--
        j++
    }
    
    return true
}

在这里插入图片描述

时间复杂度: O ( n ! ) O(n!) O(n!)
空间复杂度: O ( n ) O(n) O(n)

可以看出,除了验证棋盘合法性的代码,剩下来部分就是按照回溯法模板来的。

总结

本题是我们解决棋盘问题的第一道题目。

如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手,可能知道要用回溯法,但也不知道该怎么去搜。

这里我明确给出了棋盘的宽度就是for循环的长度(列数),递归的深度就是棋盘的高度(行数),这样就可以套进回溯法的模板里了。

大家可以在仔细体会体会!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值