想了解更多数据结构以及算法题,可以关注微信公众号“数据结构和算法”,每天一题为你精彩解答。也可以扫描下面的二维码关注

八皇后的来源

八皇后问题是一个以国际象棋为背景的问题:如何能够在8×8的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当n = 1或n ≥ 4时问题有解。

八皇后问题最早是由国际象棋棋手马克斯·贝瑟尔(Max Bezzel)于1848年提出。第一个解在1850年由弗朗兹·诺克(Franz Nauck)给出。并且将其推广为更一般的n皇后摆放问题。诺克也是首先将问题推广到更一般的n皇后摆放问题的人之一。

问题分析

我们先不看8皇后,我们先来看一下4皇后,其实原理都是一样的,比如在下面的4*4的格子里,如果我们在其中一个格子里输入了皇后,那么在这一行这一列和这左右两边的对角线上都不能有皇后。

所以有一种方式就是我们一个个去试

第一行

比如我们在第一行第一列输入了一个皇后

第二行

第二行我们就不能在第一列和第二列输入皇后了,因为有冲突了。但我们可以在第3列输入皇后

第三行

第三行我们发现在任何位置输入都会有冲突。这说明我们之前选择的是错误的,再回到上一步,我们发现第二步不光能选择第3列,而且还能选择第4列,既然选择第3列不合适,那我们就选择第4列吧

第二行(重新选择)

第二行我们选择第4列

第三行(重新选择)

第3行我们只有选择第2列不会有冲突

第四行

我们发现第4行又没有可选择的了。第一次重试失败

第二次重试

到这里我们只有重新回到第一步了,这说明我们之前第一行选择第一列是无解的,所以我们第一行不应该选择第一列,我们再来选择第二列来试试

第一行

这一行我们选择第2列

第二行

第二行我们前3个都不能选,只能选第4列

第三行

第三行我们只能选第1列

第四行

第四行我们只能选第3列

最后我们终于找到了一组解。除了这组解还有没有其他解呢,肯定还是有的,因为4皇后是有两组解的,这里我们就不在一个个试了。

我们来看一下他查找的过程,就是先从第1行的第1列开始往下找,然后再从第1行的第2列……,一直到第1行的第n列。代码该怎么写呢,看到这里估计大家都已经想到了,这不就是一棵N叉树的前序遍历吗,我们来看下,是不是下面这样的。


我们先来看一下二叉树的前序遍历怎么写,不明白的可以参考下373,数据结构-6,树

public static void preOrder(TreeNode tree) {if (tree == null)return;System.out.printf(tree.val + "");preOrder(tree.left);preOrder(tree.right);
}

二叉树是有两个子节点,那么N叉树当然是有N个子节点了,所以N叉树的前序遍历是这样的

public static void preOrder(TreeNode tree) {if (tree == null)return;System.out.printf(tree.val + "");preOrder("第1个子节点");preOrder("第2个子节点");……preOrder("第n个子节点");
}

如果N是一个很大的值,这样写要写死人了,我们一般使用循环的方式

public static void preOrder(TreeNode tree) {if (tree == null)return;System.out.printf(tree.val + "");for (int i = 0; i <n ; i++) {preOrder("第i个子节点");}
}

搞懂了上面的分析过程,那么这题的代码轮廓就呼之欲出了

private void solve(char[][] chess, int row) {"终止条件"return;for (int col = 0; col < chess.length; col++) {//判断这个位置是否可以放皇后if (valid(chess, row, col)) {//如果可以放,我们就把原来的数组chess复制一份,char[][] temp = copy(chess);//然后在这个位置放上皇后temp[row][col] = 'Q';//下一行solve(temp, row + 1);}}
}

我们来分析下上面的代码,因为是递归所以必须要有终止条件,那么这题的终止条件就是我们最后一行已经走完了,也就是

if (row == chess.length) {return;
}

第7行就是判断在这个位置我们能不能放皇后,如果不能放我们就判断这一行的下一列能不能放……,如果能放我们就把原来数组chess复制一份,然后把皇后放到这个位置,然后再判断下一行,这和我们上面画图的过程非常类似。注意这里的第9行为什么要复制一份,因为数组是引用传递,这涉及到递归的时候分支污染问题(后面有时间我会专门写一篇关于递归的时候分支污染问题)。当然不复制一份也是可以的,我们下面再讲。当我们把上面的问题都搞懂的时候,代码也就很容易写出来了,我们来看下N皇后的最终代码

public void solveNQueens(int n) {char[][] chess = new char[n][n];for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)chess[i][j] = '.';solve(chess, 0);
}private void solve(char[][] chess, int row) {if (row == chess.length) {//自己写的一个公共方法,打印二维数组的,// 主要用于测试数据用的Util.printTwoCharArrays(chess);System.out.println();return;}for (int col = 0; col < chess.length; col++) {if (valid(chess, row, col)) {char[][] temp = copy(chess);temp[row][col] = 'Q';solve(temp, row + 1);}}
}//把二维数组chess中的数据测下copy一份
private char[][] copy(char[][] chess) {char[][] temp = new char[chess.length][chess[0].length];for (int i = 0; i < chess.length; i++) {for (int j = 0; j < chess[0].length; j++) {temp[i][j] = chess[i][j];}}return temp;
}//row表示第几行,col表示第几列
private boolean valid(char[][] chess, int row, int col) {//判断当前列有没有皇后,因为他是一行一行往下走的,//我们只需要检查走过的行数即可,通俗一点就是判断当前//坐标位置的上面有没有皇后for (int i = 0; i < row; i++) {if (chess[i][col] == 'Q') {return false;}}//判断当前坐标的右上角有没有皇后for (int i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {if (chess[i][j] == 'Q') {return false;}}//判断当前坐标的左上角有没有皇后for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {if (chess[i][j] == 'Q') {return false;}}return true;
}

代码看起来比较多,我们主要看下solve方法即可,其他的方法不看也可以,知道有这个功能就行。solve代码中其核心代码是在17-23行,上面是终止条件的判断,我们就用4皇后来测试一下

solveNQueens(4);

看一下打印的结果

我们看到4皇后的时候有两组解,其中第一组和我们上面图中分析的完全一样。

4皇后解决了,那么8皇后也一样,我们只要在函数调用的时候传入8就可以了。同理,要想计算10皇后,20皇后,100皇后……也都是可以的。

回溯解决

上面代码中每次遇到能放皇后的时候,我们都会把原数组复制一份,这样对新数据的修改就不会影响到原来的,也就是不会造成分支污染。但这样每次尝试的时候都都把原数组复制一份,影响效率,有没有其他的方法不复制呢,是有的。就是每次我们选择把这个位置放置皇后的时候,如果最终不能成功,那么返回的时候我们就还要把这个位置还原。这就是回溯算法,也是试探算法。我们来看下代码

private void solve(char[][] chess, int row) {if (row == chess.length) {//自己写的一个公共方法,打印二维数组的,// 主要用于测试数据用的Util.printTwoCharArrays(chess);System.out.println();return;}for (int col = 0; col < chess.length; col++) {if (valid(chess, row, col)) {chess[row][col] = 'Q';solve(chess, row + 1);chess[row][col] = '.';}}
}

主要来看下11-13行,其他的都没变,还和上面的一样。这和我们之前讲的391,回溯算法求组合问题很类似。他是先假设[row][col]这个位置可以放皇后,然后往下找,无论找到找不到最后都会回到这个地方,因为这里是递归调用,回到这个地方的时候我们再把它复原,然后走下一个分支。最后我们再来看下使用回溯算法解N皇后的完整代码

public void solveNQueens(int n) {char[][] chess = new char[n][n];for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)chess[i][j] = '.';solve(chess, 0);
}private void solve(char[][] chess, int row) {if (row == chess.length) {//自己写的一个公共方法,打印二维数组的,// 主要用于测试数据用的Util.printTwoCharArrays(chess);System.out.println();return;}for (int col = 0; col < chess.length; col++) {if (valid(chess, row, col)) {chess[row][col] = 'Q';solve(chess, row + 1);chess[row][col] = '.';}}
}//row表示第几行,col表示第几列
private boolean valid(char[][] chess, int row, int col) {//判断当前列有没有皇后,因为他是一行一行往下走的,//我们只需要检查走过的行数即可,通俗一点就是判断当前//坐标位置的上面有没有皇后for (int i = 0; i < row; i++) {if (chess[i][col] == 'Q') {return false;}}//判断当前坐标的右上角有没有皇后for (int i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {if (chess[i][j] == 'Q') {return false;}}//判断当前坐标的左上角有没有皇后for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {if (chess[i][j] == 'Q') {return false;}}return true;
}

总结

8皇后问题其实是一道很经典的回溯算法题,我们这里并没有专门针对8皇后来讲,我们这里讲的是N皇后,如果这道题搞懂了,那么8皇后自然也就懂了,因为这里的N可以是任何正整数。

递归一般可能会有多个分支,我们要保证每个分支的修改不会污染其他分支,也就是不要对其他分支造成影响,这一点很重要。由于一些语言中除了基本类型以外,其他的大部分都是引用传递,所以我们在一个分支修改之后可能就会对其他分支产生一些我们不想要的垃圾数据,这个时候我们就有两中解决方式,一种就是我们上面讲到的第一种,复制一份新的数据,这样每个分支都会产生一些新的数据,所以即使修改了也不会对其他分支有影响。还一种方式就是我们每次使用完之后再把它复原,一般的情况下我们都会选择第二种,因为这种代码更简洁一些,也不会重复的复制数据,造成大量的垃圾数据。

java 八皇后问题以及N皇后问题相关推荐

  1. 【恋上数据结构】回溯、剪枝(八皇后、n皇后)、LeetCode51.N皇后、LeetCode52.N皇后 II

    回溯 回溯(Back Tracking) 提出八皇后问题(Eight Queens) 初步思路一:暴力出奇迹 初步思路二:根据题意减少暴力程度 初步思路三:回溯法(回溯+剪枝) 四皇后 - 回溯法图示 ...

  2. 八皇后加强版:每个皇后最多攻击一个其它的皇后

    想必搞OI/ACM的朋友都应该知道八皇后问题,这是学习编程的必修课程之一:在国际象棋棋盘上最多可以放置多少个互不攻击的皇后(皇后可以攻击它所在的行.列.对角线方向上的棋子)?显然,能够放置的皇后数不超 ...

  3. Java八岁生日快乐!

    这是另外一段漫长的旅程,但是昨天,恰好是Java 7发布两年零七个月零十八天之后,我们现在有了可用于生产的构建 Java 8可供下载 ! 这个新的主要版本包含一些新功能和增强功能,这些功能和增强功能可 ...

  4. Java八种基本数据类型的大小,以及封装类,自动装箱/拆箱的用法?

    参考:http://blog.csdn.net/mazhimazh/article/details/16799925 1. Java八种基本数据类型的大小,以及封装类,自动装箱/拆箱的用法? 原始类型 ...

  5. 关于Java八种原始数据类型

    关于Java八种原始数据类型 Java八种原始数据类型 int类型 八进制 十六进制 浮点数类型 布尔类型 字符类型 Java八种原始数据类型 Java中的原始数据类型一共有八种,分别为byte(8b ...

  6. Java八种基本数据类型对应的包装类

    目录 前言: 八种基本数据类型对应的包装类: 包装类的常用方法: 有关包装类的注意事项: 参考文献: 前言: 思考:有没有这种需求:调用doSome()方法的时候需要传一个数字进去.但是数字属于基本数 ...

  7. Java八种基本数据类型定义与取值范围

    基本数据类型也称为简单数据类型,Java语言中有八种基本数据类型, 分别为 boolean.byte.short.char.int.long.float.double,这八种基本数据类型通常分为四大类 ...

  8. Java八种基本数据类型

    基本数据类型 Java八种基本数据类型 1)四种整数类型(byte.short.int.long):(注意: 一个字节byte = 8位bit) !    byte:8 位,用于表示最小数据单位,如文 ...

  9. JAVA八种基本数据类型+三种引用数据类型

    一.Java八种基本数据类型 在程序设计中经常用到一系列类型,把它们存储在堆栈中,加快程序的运行效率,它们就是基本数据类型.Java提供了8中基本数据类型,分别是:boolean.char.byte. ...

  10. java八种基本数据类型及包装类详解

    详解一.八种基本数据类型常识 1.1.基本常识表 对于上图有以下几点需要注意: java八种基本数据类型分为四类八种,四类分别为整型.浮点型.布尔型.字符型:八种分别为byte.short.int.l ...

最新文章

  1. java书籍_Java程序员必看的 13 本 Java 书籍!
  2. 浪潮英特尔在德国发布KEEP升级计划 用户可提前体验英特尔KNM
  3. MVC专题研究(三)——数据绑定和传送
  4. 苹果也像谷歌一样,玩起了自己的X
  5. android 全局 socket,学习Android socket通信之如何解决中文乱码
  6. 什么是 NIO? NIO 和 BIO、AIO 之间的区别是什么?NIO主要用来解决什么问题?
  7. php性能优化 --- laravel 性能优化
  8. 整理Excel表格中的批注
  9. linux开机脚本文件下载,linux 开机启动脚本
  10. SpringBoot实战(三):整合Mybatis配置多数据源
  11. 项目管理系列文章——关于软件工程在软件整个生命周期的位置
  12. cad统计多线段总长度插件_新手入门,学习CAD必须掌握,教你使用标注命令,绘图效率翻一倍...
  13. 2018华为软挑参赛体验
  14. 负温度相对湿度与绝对湿度换算
  15. 人称代词的各种形式与用法
  16. 苹果的名字测试软件,教你iOS12人脸识别测颜值捷径安装使用教程及iPhone XR修改AppleID姓名教程...
  17. STM32 CAN总线通信学习笔记(一)
  18. ContentObserver去实现拒收短信或短信黑名单等功能
  19. matlab传递函数的分数次方,matlab如何画一个幂函数的曲线?f(x)=(x1)*(x2)^,matlab中如何画出幂函数指数为分数时比如y=x^(1/3...
  20. MySQL备份和还原操作

热门文章

  1. python编写一个汽车类_编写类-汽车类
  2. android手机变windows8,安卓手机如何把手机界面投屏到windows8/10电脑上
  3. 很好用的数据库数据字典【可导出为word , excel文件】
  4. 经纬度相关公式及实现
  5. 为生还者庆幸,为往生者祈祷
  6. Delphi 2005 失望的尝鲜!
  7. 现代笑话二则:1.宿舍找钱;2.不漂亮的漂亮。
  8. 怎么将自己的头像p到特定的背景图_怎么使用PS抠图,把扣出来的图片P到另张图上...
  9. IP和局域网 简单了解
  10. 怎么设置不同页的页眉