N皇后问题

https://www.bilibili.com/video/BV1ZK411K7A8?t=794

首先十分感谢B站小姐姐精彩的讲课内容,再结合一位兄弟通俗易懂的代码,我写了这份java版的N皇后问题的代码,供大家也供自己参考学习。

我们以四皇后问题为例,其主要思路就是:在这个4×4的棋盘上,我们每放置一个皇后,那么就把这个点标记为已放置皇后,然后把该皇后的所在行、所在列以及所在的两条对角线的所有点全部标记为其可攻击位,再递归地从第二行开始重复上一过程,寻找未被标记为可攻击位的点放置皇后。若不存在这样的点,那么就说明我们上一步放置皇后的位置是有误的,所以这时候就需要回到上一状态重新放置位置,这个过程,我们称之为回溯法。

其中,我们可以用一个n×n的数组来存储攻击位,再用一个char类型的二维数组存储哪些点放置了皇后,其中,‘Q’表示该点放置了皇后,‘.’表示该点未放置皇后。

现在我们来看看实现这个问题的一些方法:

1 private void copy(int[][] arr, int[][] val, int n){
2         for(int i = 0;i < n;++i){
3             for(int j = 0;j < n;++j){
4                 arr[i][j] = val[i][j];
5             }
6         }
7     }

这个是用来备份的,我们想要回溯,就需要回到上一状态,那么如何寻找上一状态呢?在放置皇后之前,我们需要备份当前的已标记攻击位的所有点和已放置了皇后的所有点。这个方法就是用来实现这个的。

1 private void print(int n){
2         for(int i = 0;i < n;++i){
3             for(int j = 0;j < n;++j){
4                 System.out.print(queen[i][j]+" ");
5             }
6             System.out.println();
7         }
8         System.out.println();
9     }

这个方法是用来打印最终棋盘的,使棋盘可视化。

 1 private void updateAttack(int x, int y, int n){  
 2         int[] dx = {-1, -1, -1, 0, 1, 1, 1, 0};  //行方向
 3         int[] dy = {-1, 0, 1, 1, 1, 0, -1, -1};  //列方向
 4         attack[x][y] = 1;
 5         for(int i = 1;i < n;++i){
 6             for(int j = 0;j < 8;++j){
 7                 int nx = x + i * dx[j];
 8                 int ny = y + i * dy[j];
 9                 if(nx >= 0 && nx < n && ny >= 0 && ny < n){
10                     attack[nx][ny] = 1;
11                 }
12             }
13         }
14     }

这个方法用来实现更新攻击位数组,即attack数组。我们需要设置八个方向(上下左右左上左下右上右下)。至于为什么dx和dy数组是那样的,我们可以这么想:假设一个点的坐标是(x,y),那么其上面的点的坐标就是(x-1,y),右边的点(x,y+1),左边的点(x,y-1),下面的点(x+1,y).其余位置可以类似地推导出来。

然后,我们需要以当前点为起始坐标点,从八个方向向外延伸,这也就是两层for循环的由来。由于初始化attack数组时里面所有的元素都为0,因此标记攻击位时我们就把值设为1.

1 public void init(int n){
2         for(int i = 0;i < n;++i){
3             for(int j = 0;j < n;++j){
4                 attack[i][j] = 0;
5                 queen[i][j] = '.';
6             }
7         }
8     }

这个方法很简单,就是用来初始化棋盘的。

 1  public void dfs(int k, int n){
 2         if(k == n){
 3             print(n);
 4             count++;
 5             return;
 6         }
 7         for(int i = 0;i < n;++i){  //这个for循环可以保证程序尝试各种可能的摆法
 8             if(attack[k][i] == 0){
 9                 int[][] temp = new int[10][10];
10                 copy(temp, attack, n); //先备份
11                 queen[k][i] = 'Q'; //在这个位置放置皇后
12                 updateAttack(k, i, n); // 因为上一步放置了皇后,所以需要更新attack数组
13                 dfs(k + 1, n); //递归下一行,继续放置皇后
14                 copy(attack, temp, n); //递归完成后,需要回溯,因此需要回到原来的状态
15                 queen[k][i] = '.';
16             }
17         }
18     }

这个就是核心代码了。k表示行数,也就是皇后数,一旦k与n相等,就表示我们找到了一组解,这个时候就把解的数量自增。如果不相等,这很正常。我们就要通过一个for循环,遍历这个棋盘所有的列。备份数组、放置皇后、更新攻击位、这些都做完后,遍历下一行,重复以上工作。同时还要回溯,遍历完后,如果下一行没有解,那么我们是需要回到原来的状态的,所以递归后我们还需要复原,回到原来的状态。如果找到了解,没关系,解的数量增加后方法照样会return,这个时候我们就需要接着找下一组解。所以也还是需要回到原来的状态。

试想一下,如果不回到原来的状态,那么在错误的位置上占据着皇后,是不是我们永远也无法接近真相了?所以狠的时候必须要狠,把错误位置的皇后去掉。

来一波完整代码:

  1 package com.hw.list0710;
  2 
  3 import java.util.Scanner;
  4 
  5 public class Queens {
  6     private int[][] attack = new int[10][10];
  7     private char[][] queen = new char[10][10];
  8     public static int count;
  9     /**
 10      * 拷贝数组,用来备份
 11      * @param arr
 12      * @param val
 13      * @param n
 14      */
 15     private void copy(int[][] arr, int[][] val, int n){
 16         for(int i = 0;i < n;++i){
 17             for(int j = 0;j < n;++j){
 18                 arr[i][j] = val[i][j];
 19             }
 20         }
 21     }
 22 
 23     /**
 24      * 打印放置了皇后的棋盘
 25      * @param n
 26      */
 27     private void print(int n){
 28         for(int i = 0;i < n;++i){
 29             for(int j = 0;j < n;++j){
 30                 System.out.print(queen[i][j]+" ");
 31             }
 32             System.out.println();
 33         }
 34         System.out.println();
 35     }
 36 
 37     /**
 38      * 设置皇后的攻击方向
 39      * @param x
 40      * @param y
 41      * @param n
 42      */
 43     private void updateAttack(int x, int y, int n){  
 44         int[] dx = {-1, -1, -1, 0, 1, 1, 1, 0};  //行方向
 45         int[] dy = {-1, 0, 1, 1, 1, 0, -1, -1};  //列方向
 46         attack[x][y] = 1;
 47         for(int i = 1;i < n;++i){
 48             for(int j = 0;j < 8;++j){
 49                 int nx = x + i * dx[j];
 50                 int ny = y + i * dy[j];
 51                 if(nx >= 0 && nx < n && ny >= 0 && ny < n){
 52                     attack[nx][ny] = 1;
 53                 }
 54             }
 55         }
 56     }
 57 
 58     /**
 59      * 初始化棋盘
 60      * @param n
 61      */
 62     public void init(int n){
 63         for(int i = 0;i < n;++i){
 64             for(int j = 0;j < n;++j){
 65                 attack[i][j] = 0;
 66                 queen[i][j] = '.';
 67             }
 68         }
 69     }
 70 
 71     /**
 72      * 放置皇后的核心代码
 73      * @param k
 74      * @param n
 75      */
 76     public void dfs(int k, int n){
 77         if(k == n){
 78             print(n);
 79             count++;
 80             return;
 81         }
 82         for(int i = 0;i < n;++i){  //这个for循环可以保证程序尝试各种可能的摆法
 83             if(attack[k][i] == 0){
 84                 int[][] temp = new int[10][10];
 85                 copy(temp, attack, n); //先备份
 86                 queen[k][i] = 'Q'; //在这个位置放置皇后
 87                 updateAttack(k, i, n); // 因为上一步放置了皇后,所以需要更新attack数组
 88                 dfs(k + 1, n); //递归下一行,继续放置皇后
 89                 copy(attack, temp, n); //递归完成后,需要回溯,因此需要回到原来的状态
 90                 queen[k][i] = '.';
 91             }
 92         }
 93     }
 94 
 95     public static void main(String[] args) {
 96         Scanner s = new Scanner(System.in);
 97         Queens queen = new Queens();
 98         count = 0;
 99         int n = s.nextInt();
100         s.close();
101         queen.init(n);
102         queen.dfs(0, n);
103         System.out.println("摆法共有:"+count+"种");
104     }
105 }

来看看效果:

 

posted @ 2021-07-15 20:08  EvanTheBoy  阅读(113)  评论(0)    收藏  举报