力扣51题:N皇后

本文记录的是我关于这一道题目的代码理解

1.题目描述

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

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

来源:力扣(LeetCode
链接:51. N 皇后 - 力扣(LeetCode)

2.题解

class Solution {
    List<List<String>> res;
    int n;
    boolean[] main;
    boolean[] sub;
    boolean[] col;
    public List<List<String>> solveNQueens(int n) {
        res = new ArrayList<>();
        if (n == 0) {
            return res;
        }

        this.n = n;
        this.col = new boolean[n];
        this.sub = new boolean[2 * n - 1];
        this.main = new boolean[2 * n - 1];
        Deque<Integer> deque = new ArrayDeque<>();
        dfs(0, deque);
        return res;
    }

    public void dfs(int row, Deque<Integer> deque) {
        if (row == n) {
            List<String> strList = Get(deque);
            res.add(strList);
            return;
        }

        for (int j = 0;j < n;j++) {
            if (!col[j] && !main[row - j + n - 1] && !sub[row + j]) {
                col[j] = true;
                main[row - j + n - 1] = true;
                sub[row + j] = true;
                deque.offerLast(j);
                dfs(row + 1, deque);
                // 回溯
                col[j] = false;
                main[row - j + n - 1] = false;
                sub[row + j] = false;
                deque.pollLast();
            }
        }
    }
    public List<String> Get(Deque<Integer> deque) {
        List<String> board = new ArrayList<>();
        for (int num : deque) {
            StringBuilder sb = new StringBuilder();
            sb.append(".".repeat(Math.max(0, n)));
            sb.replace(num, num + 1, "Q");
            board.add(sb.toString());
        }
        return board;
    }
}

3.题目思路与代码讲解

本题目重点为回溯和深度优先搜索,回溯是在递归中不可避免会出现的一种现象,在回溯题目中,相当于在对一个或多个隐形的树或图进行搜索

回溯算法并不是高效的算法,本题的时间复杂度就到了O(n!),回溯算法会对所有的可能进行枚举,并且对完全不可能得到结果的递归树分支进行剪枝(就是这个分支被pass掉了,以后不会再走了,因为这个分支无论如何都得不到结果)

假设再一次遍历中,算法发现row=3这一行走不通,无论如何也得不到结果,就会回到上一行

让我们再假设一下,如果上一行row=2的地方有一个皇后,皇后的col=1,那么说明皇后的位置row=2且col=1会导致下一行没有合法的位置可以放皇后,因为每一行都要放一个皇后的缘故,就让row=2的这个皇后在合法的情况下移动到col=2,之后再到下一层挨个枚举合法的位置,如果在row=2且col=2的位置下一行还是没有合法的位置放皇后,就让row=2的皇后在合法的条件下再次往右移动使得col=3,如果row=2的皇后尝试了所有的列都会使得row=3的皇后没有位置可以放,这时候就是row=1皇后的责任了,以此类推

如果还是很迷茫,可以见力扣关于此题的枚举树讲解,传送:深度优先搜索 - LeetBook - 力扣(LeetCode)全球极客挚爱的技术成长平台

之后,让我们再来看一下困难的点

根据国际象棋规则,要保证皇后不互相攻击的充要条件是使得皇后所在行,列,两条斜边都不得有其他皇后,行列的判断还算简单,只需要设置布尔数组即可,但是两条斜边判断就不简单了

我们下面将从左上到左下的对角线叫做主对角线,右上到右下的对角线叫做副对角线(不过话说对角线似乎没有方向),我们可以发现一共有2n-1条主对角线,同样,有2n-1条副对角线,

如果你仔细地话,你会发现在主对角线的任意一条对角线的横坐标-纵坐标是一个定值,范围是      

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        c\in[-(2n-1),2n-1]

而副对角线的横坐标+纵坐标也是一个定值,索引范围是

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​               c\in[0,2n-2]

我们发现,主对角线n\geqslant 0\Rightarrow -(n-1)\leqslant 0,其中n\in\mathbb{Z},而索引必然大于等于零,我们必须考虑到对角线下标为负数的这个问题,解决方法就是在判断某一个坐标属于哪条主对角线时使用公式

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​          row-col+n-1

 并且把主对角线索引区间翻转,变为c\in[0,2n-2],与副对角线索引区间相同

我们可以看一下主方法代码了

public List<List<String>> solveNQueens(int n) {
    res = new ArrayList<>();
    if (n == 0) {
        return res;
    }

    this.n = n;
    this.col = new boolean[n];
    this.sub = new boolean[2 * n - 1];
    this.main = new boolean[2 * n - 1];
    Deque<Integer> deque = new ArrayDeque<>();
    dfs(0, deque);
    return res;
}

其中因为每一行只能放置一位皇后,所以我们只需要管每个列最多只能有一位皇后就可以了,这里根据传进来的n来new了各个boolean[]用来查重的变量

其中main负责主对角线查重,sub负责副对角线查重,之后进行DFS

public void dfs(int row, Deque<Integer> deque) {
    if (row == n) {
        List<String> strList = Get(deque);
        res.add(strList);
        return;
    }

    for (int j = 0;j < n;j++) {
        if (!col[j] && !main[row - j + n - 1] && !sub[row + j]) {
            col[j] = true;
            main[row - j + n - 1] = true;
            sub[row + j] = true;
            deque.offerLast(j);
            dfs(row + 1, deque);
            // 回溯
            col[j] = false;
            main[row - j + n - 1] = false;
            sub[row + j] = false;
            eque.pollLast();
        }
    }
}

这里是整个算法的核心实现

if (row == n) {
    List<String> strList = Get(deque);
    res.add(strList);
    return;
}

首先,递归不能一直递下去不然早晚栈溢出,设置终止条件,终止条件很容易想到,就是所有皇后都归位时,说明这是一种解法,队列deque中存储的是这个解法中每一行皇后的位置索引,使用下文中的Get方法把deque转为力扣答案的形式,由于可能有多种解法,把解法先放进res

for (int j = 0;j < n;j++) {
    if (!col[j] && !main[row - j + n - 1] && !sub[row + j]) {
        col[j] = true;
        main[row - j + n - 1] = true;
        sub[row + j] = true;
        deque.offerLast(j);
        dfs(row + 1, deque);
        // 回溯
        col[j] = false;
        main[row - j + n - 1] = false;
        sub[row + j] = false;
        eque.pollLast();
    }
}
!col[j] && !main[row - j + n - 1] && !sub[row + j]

这个判断删去了非法位置,如果皇后放在非法位置会与其他皇后干架,结果就错了,因此这个判断检测当前列有没有其他皇后,当前主对角线和副对角线有没有其他皇后,如果没有说明当前位置合法,直接放置

col[j] = true;
main[row - j + n - 1] = true;
sub[row + j] = true;
deque.offerLast(j);
dfs(row + 1, deque);

这里体现了DFS的递归模板,放置皇后后,直接将当前列,当前主对角线和副对角线设为“已占”,并且递归调用DFS进行下一层的搜索

// 回溯
col[j] = false;
main[row - j + n - 1] = false;
sub[row + j] = false;
eque.pollLast();

当尝试完了下面的所有组合之后,就要尝试下一个位置,因此删除所有自己留下的蛛丝马迹,再次进行下一轮的枚举

接下来就是Get方法,这个方法的作用是在找到一个解法时把队列中的皇后位置索引转换为整个棋盘

public List<String> Get(Deque<Integer> deque) {
    List<String> board = new ArrayList<>();
    for (int num : deque) {
        StringBuilder sb = new StringBuilder();
        sb.append(".".repeat(Math.max(0, n)));
        sb.replace(num, num + 1, "Q");
        board.add(sb.toString());
    }
    return board;
}

这里采用的思路是一步到位,先直接把整个列表设为空(即“.”),之后再把皇后位置的"."替换为"Q",那么,这个

sb.append(".".repeat(Math.max(0, n)));

是什么鬼?这是为了防止n为负数时产生的异常,保证产生正确的结果

4.复杂度分析

时间复杂度:O(n!),其中n为皇后的数量

空间复杂度:O(n),递归调用栈的深度最多为n

3.结语

感谢您的观看与支持!这只是鄙人对这道题的解法,肯定不是此题最优解,但还是能感谢您能抽出时间来看我的小破文章!

如有错误感谢在评论区中指出!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值