浅谈Java实现回溯算法之八皇后问题

目录
  • 一、前言
  • 二、浅谈递归
  • 三、回溯算法
  • 四、八皇后问题
  • 五、八皇后变种
  • 六、总结

一、前言

说起八皇后问题,它是一道回溯算法类的经典问题,也可能是我们大部分人在上数据结构或者算法课上遇到过的最难的一道题……

二、浅谈递归

对于递归算法,我觉得掌握递归是入门数据结构与算法的关键,因为后面学习很多操作涉及到递归,例如链表的一些操作、树的遍历和一些操作、图的dfs、快排、归并排序等等。

递归的实质还是借助栈实现一些操作,利用递归能够完成的操作使用栈都能够完成,并且利用栈的话可以很好的控制停止,效率更高(递归是一个来回的过程回来的时候需要特判)。

递归实现和栈实现操作的区别,递归对我们来说更简洁巧妙,并且用多了会发现很多问题的处理上递归的思考方式更偏向人的思考方式,而栈的话就是老老实实用计算机(数据结构特性)的思维去思考问题。这个你可以参考二叉树的遍历方式递归和非递归版本,复杂性一目了然。

从递归算法的特征上来看,递归算法的问题都是父问题可以用过一定关系转化为子问题。即从后往前推导的过程,一般通过一个参数来表示当前的层级。

而递归的主要特点如下:

  • 自己调用自己
  • 递归通常不在意具体操作,只关心初始条件和上下层的变化关系。
  • 递归函数需要有临界停止点,即递归不能无限制的执行下去。通常这个点为必须经过的一个数。
  • 递归可以被栈替代。有些递归可以优化。比如遇到重复性的可以借助空间内存记录而减少递归的次数。

而通常递归算法的一个流程大致为:

定义递归算法及参数

- 停止递归算法条件

- (可存在)其他逻辑

- 递归调用(参数需要改变)

- (可存在)其他逻辑

三、回溯算法

谈完递归,你可能明白有这么一种方法可以使用,但你可能感觉单单的递归和八皇后还是很难扯上关系,是的没错,所以我来讲回溯算法了。

算法界中,有五大常用算法:贪心算法、分治算法、动态规划算法、回溯算法、分支界限算法。咱们回溯算法就是五大之一,因为回溯算法能够解决很多实际的问题,尽管很多时候复杂度可能不太小,但大部分情况都能得到一个不错的结果。

对于回溯法的定义,百度百科是这么定义的:

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

对于回溯法,它的核心就是试探和复原。这个自动化的过程就是利用递归去执行,在递归函数执行前去修改尝试,满足条件后向下递归试探,试探完毕后需要将数值复原。在这个试探的过程中找到我们所需要的一个或者所有解。这个我们也俗称暴力。

啥?没听懂?好,那我就再讲讲,你应该知道深度优先搜索(dfs)吧?其实回溯算法就是一种特殊的dfs。之所以叫回溯,就是因为这类算法在运用递归都有个复原的过程,所以前面的操作就相当于试探一样。而这类算法一般常常配对一个或多个boolean类型的数组用来标记试探途中用过的点。

举个例子,我们知道回溯算法用来求所有数字的排列顺序。我们分析其中一个顺序。比如数列6 8 9这个序列的话,我们用来求它的排列顺序。

对于代码块来说,这可能很容易实现:

import java.util.Arrays;

public class test {
    public static void main(String[] args) {
        int arr[]={6,8,9};//需要排列组合的数组
        int val[]={0,0,0};//临时储存的数组
        boolean jud[] = new boolean[arr.length];// 判断是否被用
        dfs(arr,val, jud,  0,"");//用一个字符串长度更直观看结果
    }

    private static void dfs(int[] arr, int val[],boolean[] jud, int index,String s) {
        System.out.println(s+Arrays.toString(val));
        if (index == arr.length){ }//停止递归条件
        else{
            for (int i = 0; i < arr.length; i++) {
                if (!jud[i]) {//当前不能用的
                    int team=val[index];
                    val[index] = arr[i];
                    jud[i] = true;// 下层不能在用
                    dfs(arr, val, jud, index + 1,s+"  _  ");
                    jud[i] = false;// 还原
                    val[index]=team;
                }
            }
        }
    }
}

而执行的结果为:

这里再配张图理解:

而通常回溯算法的一个流程大致为:

定义回溯算法及参数

- (符合条件)跳出

- (不符合)不跳出:

- - 遍历需要操作的列表&&该元素可操作&&可以继续试探

- - - 标记该元素已使用以及其他操作

- - - 递归调用(参数改变)

- - - 清除该元素标记以及其他操作

也就是在使用数组进行回溯的时候,使用过的时候需要标记子递归不能再使用防止死循环,而当回来的时候需要解封该位置,以便该编号位置被其他兄弟使用之后这个数值在后面能够再次使用!而如果使用List或者StringBuilder等动态空间用来进行回溯的时候记得同样的复原,删了要记得增,减了要记得加。搞明白这些,我想回溯算法也应该难不倒你了吧。

四、八皇后问题

掌握了回溯算法的关键,八皇后问题多思考就可以想的出来了。前面的讲解都是为了解决八皇后问题做铺垫。首先,我们认真的看下八皇后问题描述。

八皇后问题(英文:Eight queens),是由国际西洋棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。

问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。如果经过±90度、±180度旋转,和对角线对称变换的摆法看成一类,共有42类。计算机发明后,有多种计算机语言可以编程解决此问题。

我们该怎么思考这种问题呢?也就是从何入手呢?

  • 从限制条件入手

八皇后问题有以下限制条件:

  • 8 x 8的方格
  • 每行一个,共八行(0-7)
  • 每列一个,共八列(0-7)
  • 每左斜杠一个,共十五左斜杠(0-14)
  • 每右斜杠一个,共十五右斜杠(0-14)

当看到这些限制条件,肯定想到这么多限制条件需要判断。判断的话当然就是借助boolean数组啦。还是一维的8个大小,所以我们首先用4个boolean数组用来判断各自的条件是否被满足。

表示这个图的话我们可以使用一个int类型数组表示,0表示没有,1表示有皇后。

那么如何去设计这个算法呢?这个并不是每个格子都有数字,所以在进行回溯的时候不应该每个格子每个格子进行向下递归(同行互斥),也就是递归到当前层的时候,循环遍历该层的八种情况进行试探(每个都试探),如果不满足条件的就不操作而被终止掉,但该行每个满足条件的需要递归的时候需要进入到下一行。

当然你需要提前知道当前位置横纵坐标怎们知道对应的boolean位置(位置从0号开始计算)。例如位置p(x,y)中对应的位置为:

  • hang[] :x每一行就是对应的i。
  • lie[] :y每一列就是对应的j。
  • zuoxie[] :x+y规定顺序为左上到右下
  • youxie[] :x+(7-y)规定顺序为右上到左下(个人习惯)

好啦,该算法的实现代码为:

import java.util.Arrays;

public class EightQueens {
    static  int allnum=0;
    public static void main(String[] args) {
        boolean hang[]=new boolean[8];//行
        boolean lie[]=new boolean[8];//列
        boolean zuoxie[]=new boolean[15];//左斜杠
        boolean youxie[]=new boolean[15];//右斜杠
        int map[][]=new int[8][8];//地图
        dfs(0,hang,lie,zuoxie,youxie,map);//进行下去
    }

    private static void dfs(int hindex, boolean[] hang, boolean[] lie, boolean[] zuoxie, boolean[] youxie, int[][] map) {
        if(hindex==8){
            allnum++;
            printmap(map);//输出map
        }
        else{
            //hindex为行  i为具体的某一列
            for(int i=0;i<8;i++)
            {
                if(!hang[hindex]&&!lie[i]&&!zuoxie[hindex+i]&&!youxie[hindex+(7-i)])
                {
                    hang[hindex]=true;//试探
                    lie[i]=true;
                    zuoxie[hindex+i]=true;
                    youxie[hindex+(7-i)]=true;
                    map[hindex][i]=1;
                    dfs(hindex+1,hang,lie,zuoxie,youxie,map);//dfs
                    hang[hindex]=false;//还原
                    lie[i]=false;
                    zuoxie[hindex+i]=false;
                    youxie[hindex+(7-i)]=false;
                    map[hindex][i]=0;
                }
            }
        }
    }
    //输出地图
    private static void printmap(int[][] map) {
        System.out.println("第"+allnum+"个排列为");
        for(int a[]:map)
        {
            System.out.println(Arrays.toString(a));
        }
    }
}

跑一边就知道到底有多少种皇后,最终是92种皇后排列方式,不得不说能用数学方法接出来的是真的牛叉。

五、八皇后变种

此时我想八皇后问题已经搞得明明白白了,但是智慧的人们总是想出各种方法变化题目想难到我们,这种八皇后问题有很多变种,例如n皇后,数独等问题。

这里就简单讲讲两数独问题的变种。

力扣36 有效的数独

像这种题需要考虑和八皇后还是很像,改成9*9,只不过在具体处理需要考虑3x3小方格

当然这题比较简单,还有一题就比较麻烦了力扣37解数独。

这一题有难度的就是需要我们每个位置都有数据都要去试探。

这种二维的回溯需要考虑一些问题,我们对于每一行每一行考虑。 每一行已经预有一些数据事先标记,在从开始试探放值,满足条件后向下递归试探。一直到结束如果都满足那么就可以结束返回数组值。

这里的话有两点需要注意的在这里提一下:

  • 用二维两个参数进行递归回溯判断起来谁加谁减比较麻烦,所以我们用一个参数index用它来计算横纵坐标进行转换,这样就减少二维递归的一些麻烦。
  • 回溯是一个来回的过程,在回来的过程正常情况需要将数据改回去,但是如果已经知道结果就没必要再该回去可以直接停止放置回溯造成值的修改(这里我用了一个isfinish的boolean类型进行判断)。

代码可以参考为:

六、总结

递归更注重一种方式,自己调用自己。回溯更注重试探和复原,这个过程一般借助递归。dfs深度优先搜素,一般用栈或者递归去实现,如果用递归可能会复原也可能不复原数据,所以回溯是深搜的一种。八皇后是经典回溯算法解决的问题,你说深度优先搜素其实也没问题,但回溯更能精准的描述算法特征。

以上就是浅谈Java实现回溯算法之八皇后问题的详细内容,更多关于Java 回溯算法 八皇后的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java实现走迷宫回溯算法

    以一个M×N的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍.设计一个程序,对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论. (1) 根据二维数组,输出迷宫的图形. (2) 探索迷宫的四个方向:RIGHT为向右,DOWN向下,LEFT向左,UP向上,输出从入口到出口的行走路径. 例子: 左上角(1,1)为入口,右下角(8,9)为出口. 可使用回溯方法,即从入口出发,顺着某一个方向进行探索,若能走通,则继续往前进:否则沿着原路退回,换一个方向继续探索,直至出口位置,求得一条通

  • java使用回溯法求解数独示例

    复制代码 代码如下: import java.util.Calendar;import java.util.Date; public class Matrix { private int matrix[][]; private long timeAfter=0;  private long timeBefore =0; public Matrix(int m[][]) {  matrix = new int[9][9];  for (int i=0; i<9 ; i++)   for(int j

  • java实现八皇后问题示例分享

    问题描述:将八个皇后放在棋盘上,任何两个皇后都不能互相攻击(即没有任何两个皇后在同一行.同一列或者同一对角线上)如图所示   在本文中,对于两道题采用了稍微不同的解决方式,但都使用的是一维数组.6.20中,要求求出一种有效布局,我建立了一个 有八个元素的一位数组,通过随意打乱数组的值,通过值与下标的比较,直至得出一个有效布局:6.22中,要求求出所有有效布局,这里我使用了八进制数,遍历了  从001234567-076543210的所有数字,通过将其转化为八进制字符串,每位与其下标相比较,输出满

  • 正则中的回溯定义与用法分析【JS与java实现】

    本文实例分析了正则中的回溯定义与用法.分享给大家供大家参考,具体如下: 关于"回溯"我也是第一次接触,对它也不算很了解.下面就把我所了解的做为一个心德记录下来,以备查看. 我们所使用的正则表达式的匹配基础大概分为:优先选择最左端(最靠开头)的匹配结果和标准的匹配量词(*.+.?和{m, n})是匹配优先的. "优先选择最左端的匹配"顾名思义就是从字符串的起始位置开始匹配直到匹配结束这是基础:"标准匹配量词"又分为"非确定型有穷自动机(N

  • java回溯算法解数独问题

    本文实例为大家分享了java回溯算法解数独问题,供大家参考,具体内容如下 下面来详细讲一下如何用回溯算法来解数独问题. 下图是一个数独题,也是号称世界上最难的数独.当然了,对于计算机程序来说,只要算法是对的,难不难就不知道了,反正计算机又不累.回溯算法基本上就是穷举,解这种数独类的问题逻辑比较简单. 不管算法懂不懂,先把类建出来,变量定义好,那放大学试卷上就是可以拿两分了. package shudu; /** * Created by wolf on 2016/3/17. */ public

  • 如何基于java语言实现八皇后问题

    这篇文章主要介绍了如何基于java语言实现八皇后问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 八皇后问题,在一个8X8的棋盘中,放置八个棋子,每个棋子的上下左右,左上左下,右上右下方向上不得有其他棋子.正确答案为92中,接下来用java语言实现. 代码如下 package eightQuen; /** * 八皇后问题 * * @author 83771 * */ public class eight { // 定义一个数组 表示棋盘 pu

  • Java基于循环递归回溯实现八皇后问题算法示例

    本文实例讲述了Java基于循环递归回溯实现八皇后问题.分享给大家供大家参考,具体如下: 运行效果图如下: 棋盘接口 /** * 棋盘接口 * @author Administrator * */ public interface Piece { abstract boolean isRow(int line); abstract boolean isCol(int line,int col); } 棋盘类: /** * 棋盘 * @author Administrator * */ public

  • 浅谈Java实现回溯算法之八皇后问题

    目录 一.前言 二.浅谈递归 三.回溯算法 四.八皇后问题 五.八皇后变种 六.总结 一.前言 说起八皇后问题,它是一道回溯算法类的经典问题,也可能是我们大部分人在上数据结构或者算法课上遇到过的最难的一道题-- 二.浅谈递归 对于递归算法,我觉得掌握递归是入门数据结构与算法的关键,因为后面学习很多操作涉及到递归,例如链表的一些操作.树的遍历和一些操作.图的dfs.快排.归并排序等等. 递归的实质还是借助栈实现一些操作,利用递归能够完成的操作使用栈都能够完成,并且利用栈的话可以很好的控制停止,效率

  • 浅谈java实现背包算法(0-1背包问题)

    0-1背包的问题 背包问题(Knapsack problem)是一种组合优化的NP完全问题.问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高.问题的名称来源于如何选择最合适的物品放置于给定背包中. 这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放. 用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值.则其状态转移方程便是: f[i][v]=max{ f[i-1][v], f

  • 浅谈Java常见的排序算法

    目录 一.直接插入排序 二. 希尔排序 三.冒泡排序 四.快速排序 五.选择排序(Selection Sort) 六.堆排序 七.归并排序 一.直接插入排序 基本思想: 将一个记录插入到已排序的有序表中,使插入后的表仍然有序 对初始关键字{49 38 65 97 76 13 27 49}进行直接插入排序 package Sort; //插入排序 public class InsertSort { public static void main(String[] args) { int [] ar

  • 浅谈Java中Unicode的编码和实现

    Unicode的编码和实现 大概来说,Unicode编码系统可分为编码方式和实现方式两个层次. 编码方式 字符是抽象的最小文本单位.它没有固定的形状(可能是一个字形),而且没有值."A"是一个字符,"€"也是一个字符.字符集是字符的集合.编码字符集是一个字符集,它为每一个字符分配一个唯一数字. Unicode 最初设计是作为一种固定宽度的 16 位字符编码.也就是每个字符占用2个字节.这样理论上一共最多可以表示216(即65536)个字符.上述16位统一码字符构成基

  • 浅谈java中OO的概念和设计原则(必看)

    一.OO(面向对象)的设计基础 面向对象(OO):就是基于对象概念,以对象为中心,以类和继承为构造机制,充分利用接口和多态提供灵活性,来认识.理解.刻划客观世界和设计.构建相应的软件系统.面向对象的特征:虽然各种面向对象编程语言相互有别,但都能看到它们对面向对象基本特征的支持, 即 "抽象.封装.继承.多态" : – 抽象,先不考虑细节 – 封装,隐藏内部实现 – 继承,复用现有代码 – 多态,改写对象行为 面向对象设计模式:是"好的面向对象设计",所谓"

  • 浅谈Java中几种常见的比较器的实现方法

    在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题. 通常对象之间的比较可以从两个方面去看: 第一个方面:对象的地址是否一样,也就是是否引用自同一个对象.这种方式可以直接使用"=="来完成. 第二个方面:以对象的某一个属性的角度去比较. 从最新的JDK8而言,有三种实现对象比较的方法: 一.覆写Object类的equals()方法: 二.继承Comparable接口,并实现compareTo()方法: 三.定义一个单独的对象比较器,继承自Comparator接口

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • 浅谈java泛型的作用及其基本概念

    一.泛型的基本概念 java与c#一样,都存在泛型的概念,及类型的参数化.java中的泛型是在jdk5.0后出现的,但是java中的泛型与C#中的泛型是有本质区别的,首先从集合类型上来说,java 中的ArrayList<Integer>和ArrayList<String>是同一个类型,在编译时会执行类型擦除,及java中的类型是伪泛型,伪泛型将会在后面介绍,其次,对于像集合中添加基本类型的数据时,例如int,会首先将int转化成Integer对象,即我们通常所说的装箱操作,在取出

  • 浅谈Java中ABA问题及避免

    本文主要研究的是关于Java中ABA问题及避免的相关内容,具体如下. 在<Java并发实战>一书的第15章中有一个用原子变量实现的并发栈,代码如下: public class Node { public final String item; public Node next; public Node(String item){ this.item = item; } } public class ConcurrentStack { AtomicReference<Node> top

  • 浅谈Java BitSet使用场景和代码示例

    一.什么是BitSet? 注:以下内容来自JDK API: BitSet类实现了一个按需增长的位向量.位Set的每一个组件都有一个boolean值.用非负的整数将BitSet的位编入索引.可以对每个编入索引的位进行测试.设置或者清除.通过逻辑与.逻辑或和逻辑异或操作,可以使用一个 BitSet修改另一个 BitSet的内容. 默认情况下,set 中所有位的初始值都是false. 每个位 set 都有一个当前大小,也就是该位 set 当前所用空间的位数.注意,这个大小与位 set 的实现有关,所以

随机推荐