Leetcode【回溯】| 51. N皇后 & 52. N皇后 II

本文深入解析了LeetCode上的N皇后问题及其变种N皇后II,提供了详细的回溯算法实现过程,包括如何使用辅助数组优化解决方案。

题目

Leetcode 51. N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。在这里插入图片描述
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例:
 输入:4
 输出:[
   [".Q…", // 解法 1
   “…Q”,
   “Q…”,
   “…Q.”],
 
   ["…Q.", // 解法 2
   “Q…”,
   “…Q”,
   “.Q…”]
 ]
 解释: 4 皇后问题存在两个不同的解法。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/n-queens
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

Leetcode 52. N皇后 II

给定一个整数 n,返回 n 皇后不同的解决方案的数量。
与上一题不同的就是,上一题返回结果是所有皇后的放置方案,这一题不需要确切的放置结果,只需要返回所有放置方案的数量就可以了。

解题

思路

分析题目: 皇后的放置有三个限制条件,就是同一行、同一列和同一个斜线的不能放置。

所以初步思路就是决策树/回溯,一行一行的放置,每一行放置一个保证了同一行不会有两个,然后在往下一行行放置的过程中,遍历每个位置,对每个位置进行选择放置的时候判断当前位置是否有同行/同列/同斜线的已经有皇后了,如果有的话就不能放置了,如果没有,则可以放置,进行下一步回溯。
 
 框架就是:在循环里执行这三步

  1. 做出当前选择
  2. 进行下一步决策(递归)
  3. 撤销当前选择(不影响下次选择),即将第一步做的改变还原

应用到这一题就是在循环里判断当前遍历到的点(i,j)是否能放置,如果能的话开始决策:

  1. 选择放置当前点(i,j),将当前点的行、列和斜线都标记为已放置
  2. 进行下一步决策,即放下一个点,即进行下一行进行放置
  3. 撤销选择, 即将第一步做的标记都撤销

51题答案

这题需要返回的是具体的放置方案,所以有两种解决方案。

  1. 我们可以用二维数组直接存储每一次决策的过程和结果,在决策的过程中通过在当前决策的二维数组中判断是否有皇后来判断是否能放当前位置,当能放置到最后一行的时候就将放置结果(二维数组)直接存入结果列表中。如下51题代码。
  2. 利用三个辅助数组,标记某行/某左上右下斜线/某左下右上斜线是否已有皇后(为什么不用行标记?因为决策的时候就是一行一行的往下的),这样避免了每次判断需要在已决策的二维数组中进行当前列和两个斜线的遍历来判断是否有皇后冲突,相当于用空间节省了时间。如下52题代码。
class Solution {
    int N;
    char[][] board;//模拟棋盘
    List<List<String>> schemes = new LinkedList<>();//存放结果
    public List<List<String>> solveNQueens(int n) {
        //经典的回溯问题
        //明确选择:每一行有n个位置可选
        //明确约束条件:在某个位置防止皇后后要确保不会被攻击,即他所在行、列和斜线不能在被放了
        //明确路径:在某一行摆放皇后时,前面已经摆放了的皇后的位置
        //明确结束条件:当每一行都成功放下了一个皇后时,说明得到了一个合法的解法
        N = n;
        board = new char[N][N];
        for(char[] line:board){
            //初始化
            Arrays.fill(line,'.');
        }
        backtrack(0);//从第一行开始放皇后
        return schemes;
    }
    //放置  即回溯的方法 r为行数
    public void backtrack(int r){
        //结束条件 能添加到最后一行
        if(r == N){
            //添加方案
            List<String> scheme = new LinkedList<>();
            for(char[] line:board){//line为每一行的数组
                scheme.add(String.valueOf(line));
            }
            schemes.add(scheme);
            return;
        }
        //选择列表
        for(int j = 0; j < N; j++){
            if(isValid(r,j)){//判断当前位置是否能放
            //注意:这三行,选择,递归,撤销选择,是框架
                board[r][j] = 'Q';
                backtrack(r+1);//放置下一行 如果不能放置到最后一行  就会返回这里,说明当前这个位置不能放置没法得到最终结果,所以要撤销选择,进行下一个位置的选择
                board[r][j] = '.';//撤销选择,以免影响同一行其他位置的摆放
            }
        }
    }
    //判断该位置是否能放
    public Boolean isValid(int x, int y){
        //同一行,判断这一行上是否已经有Q
        for(int j = 0; j < y; j++){
            if(board[x][j] == 'Q'){
                return false;
            }
        }
         // 同一列
        for (int i = 0; i < x; i++) {
            if (board[i][y] == 'Q') {
                return false;
            }
        }
        // 主对角线 左上右下
        for (int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }
        // 副对角线 左下右上
        for (int i = x - 1, j = y + 1; i >= 0 && j < N; i--, j++) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
    
}

52题答案

因为52题只需要返回放置方案的数量,所以不需要回溯的过程中同步二维数组保存放置结果,所以可以直接用上述第二种解决方案,建立三个辅助数组来标记某一列/左上右下斜线/左下右上斜线是否已有皇后。

  1. 对列来说,第i列就是col[i]
  2. 对左上右下斜线 “\” 来说,同一斜线上的坐标差 j-i 都是相等的,且从左下(n-1,0)第一条斜线差为-n+1到右上(0,n-1)最后一条斜线差为n-1, 可以看出,一共有2n-1条斜线,每条斜线差依次+1,所以可以用每个位置的坐标差来标识它是哪个斜线的。
  3. 对左下右上斜线”/“来说,同一斜线上的坐标和 j+i 都是相等的,且从左上(0,0)第一条斜线和为0到右下(n-1,n-1)最后一条斜线和为2n-1,可以看出,一共有2n-1条斜线,每条斜线和依次+1,所以可以用每个位置的坐标和来标识它是哪个斜线的。
class Solution {
    public int count = 0;
    public int totalNQueens(int n) {
        //回溯 因为每个皇后的行、列和两个斜线上都不能放置其它皇后,否则就会攻击,所以我们一行一行的放,然后设置三个数组来标记列和两个斜线上是否可以放
        boolean[] col = new boolean[n];//记录某列是否可放置,若为0则为可放,若为1则为不能放置
        //因为斜线上的坐标差或和是一致的,所以用差或和的那个索引下标
        boolean[] diagonal1 = new boolean[n*2-1];//左下右上斜线 这个斜线上横纵坐标相加和相等,并随着斜线往下,和加1,左上角(0,0)为第一个斜线,和为0,右下角(n-1,n-1)为最后一个斜线,何为2n-2;
        boolean[] diagonal2 = new boolean[n*2-1];//左上右下斜线 ,统一斜线j-i相等,从左上角第一个斜线开始差为-n+1,一直到右上角最后一个斜线结束,差为n-1
        for(int i = 0; i < n; i++){
            //从第一行开始回溯,选择放置(0,i)点
            col[i] = true;
            diagonal1[i+0] = true;
            diagonal2[i-0+n-1] = true;
            //进行下一行
            backtrack(n, 1, col,diagonal1,diagonal2);
            //撤销选择
            col[i] = false;
            diagonal1[i+0] = false;
            diagonal2[i-0+n-1] = false;
        }
        return count;
    }
    public void backtrack(int n, int r, boolean[] col, boolean[] diagonal1, boolean[] diagonal2){//n为行数,r为该放置的当前行
        if(r == n){
            //放置完最后一行,说明能放置成功
            count++;
            return;
        }
        for(int j = 0; j < n; j++){
            if(col[j] || diagonal1[r+j] || diagonal2[j-r+n-1]){
                //其中有一个为true说明这个位置就不能放
                continue;
            }else{
                col[j] = true;
                diagonal1[r+j] = true;
                diagonal2[j-r+n-1] = true;
                //下一行
                backtrack(n,r+1,col,diagonal1,diagonal2);
                //撤销
                col[j] = false;
                diagonal1[r+j] = false;
                diagonal2[j-r+n-1] = false;
            }
        }  
    }

}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值