java递归求八皇后问题解法

八皇后问题

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。

网上有很多八皇后的小游戏,不清楚规则的可以点击这里体验一把。

 

递归理解

由于我们使用经典的递归回溯算法,所以要先理解递归的调用过程,在使用递归前我们先看下普通方法的调用过程在JVM中如何体现。首先我们来看下jvm中五个重要的空间,如下图所示

这里我们主要关注栈区,当调用某个方法时会在栈区为每个线程分配独立的栈空间,而每个方法都会以栈帧的形式压入栈中,即每个方法从进入到退出都对应着一个栈帧的压栈和出栈。如下图所示

 

在每个栈帧中都会有方法独立的局部变量表,操作数栈,动态连接,返回地址等信息。如下图所示

 

理解了jvm程序栈的结构后,下面我们以图解的方式先讲解一下普通方法(理解普通方法调用过程后,再讲解递归的调用过程)的调用过程。

假设程序main方法首先调用了method1,在method1中调用了method2,在method2中调用method3。代码如下

 1 public static void main(String []args){
 2         method1();
 3     }
 4     
 5     private static void method1(){
 6         System.out.println("method1调用开始");
 7         method2();
 8         System.out.println("method1调用结束");
 9     }
10 
11     private static void method2(){
12         System.out.println("method2调用开始");
13         method3();
14         System.out.println("method2调用结束");
15     }
16     private static void method3(){
17         System.out.println("method3调用开始");
18         System.out.println("method3调用结束");
19     }

  当执行main方法时,会执行以下步骤

  1)首先将main方法压入栈中,在main方法中调用method1,方法mehod1会压入栈中,并执行打印“method1调用开始”

  2)执行到第7行时,将method2压入栈中,执行method2方法的代码打印出“method2调用开始”

  3)执行到第13行时调用method3方法,将method3方法压入栈中,执行method3方法打印“method3调用开始”,方法压入栈中的过程图解,如下图所示

 

  

  当执行到图4中的method3方法打印出“method3调用开始”后会执行以下步骤

  1)method3执行打印“method3调用结束”后method3方法体已全部执行完毕,method3方法会出栈,并根据栈帧中程序计数器记录的调用者调用本方法的所在行返回。即返回到method2的13行

  2)执行method2第14行,打印出“method2调用结束”。method2方法体执行完毕,method2方法出栈,返回到method1的第7行

  3)执行method1第8行,打印出method1调用结束。method1方法出栈,返回到main方法中第2行,main方法执行完毕,main方法出栈,整个程序运行结束

  对应图解如下

 

  根据上面的流程可知程序的运行结果为:

  method1调用开始

  method2调用开始

  method3调用开始

  method3调用结束

  method2调用结束

  method1调用结束

 

 理解了普通方法的调用过程后,下面我们来讲解递归方法的调用过程,我们都知道递归调用就是方法调用自己,当然我们也可以套用上面普通方法的流程,主观认为它是调用别的方法。

 下面以一个求n的阶乘的递归方法为例讲解调用过程,代码如下

 1 public static void main(String []args){
 2         System.out.println(fn(5));
 3     }
 4 
 5     private static int fn(int n){
 6         if(n == 1){
 7             return 1;
 8         }
 9         return fn(n-1)*n;
10     }

 下面还以图解的方式讲解递归的执行过程,为了好区分每次递归的过程,我们以传入的参数标示fn方法,如n=5时,我们假定调用fn5方法。调用过程如下图所示

 

   

  方法的调用扔以压栈的方式进行,调用fn(5)时,fn5压栈,而求fn(5)需要先调用fn(4),从而fn4压栈,依此类推,直到fn(1)方法压栈,此时if(n==1)条件成立,fn(1)方法返回。如下图 

        图10                    图11               图12                  图13

 

        图14

  执行到图14后,递归的执行过程结束,并将结果5*4*3*2*1的结果返回给main方法并输出,结果为120。

  以上就是递归的执行过程分析,其实跟普通方法的调用过程一样,只不过递归调用的方法是自己而已。

  好了,终于到了本文的重点了(铺垫做的太多),递归回溯法求八皇后解法问题

八皇后问题解法

  问题分析

  1)用代码求解八皇后问题的前提,我们要先构造出来一个8*8的二维数组,但由于八皇后问题的条件限制----任意两个皇后不能同行,所以我们可使用一个8位一维数组表示棋盘,一维数组的第n个元素即代表第n-1(从第0行开始)行,第n个元素的值即代表第n行的列值,如:0 4 7 5 2 6 1 3 ,其中0表示第0行第0列,4表示第2行第5列,7表示第3行第8列,以此类推。

  2)我们在求解的过程中,每添加一个皇后,行数加1,所以不会出现任意两个皇后处在同一行的情况,所以我们只需判断任意两个皇后不在同一列,也不在同一斜线上即可。

  3)从第0行第0列开始放第一个皇后,依此循环8个皇后,并在下一行判断,只要不跟前面所有皇后在同一列或同一斜线上即可放置皇后。

  代码实现

 1 /**
 2  * 递归法解决八皇后问题
 3  */
 4 public class BaHuangHou {
 5     private final static int max = 8;
 6     private static int array[] = new int[max];
 7     private static int count = 0;
 8     public static void main(String []args){
 9         //定义一个一位数组表示八皇后的棋盘(第n个代表第n行,值代表第n行的第m列)
10 
11         check(0);
12         System.out.printf("总共有%d种解法\n",count);
13     }
14 
15     /**
16      * 放置第n个皇后
17      * @param n
18      * @return
19      */
20     private static void check(int n){
21         if(n == max){
22             print(array);
23             return;
24         }
25         for(int i=0; i<max; i++){
26             array[n] = i;
27             if(judge(n)){
28                 check(n+1);
29             }
30         }
31     }
32     /**
33      * 判断第n个皇后是否与之前的冲突
34      * @param n
35      * @return
36      */
37     private static boolean judge(int n){
38         for(int i=0; i<n; i++){
39             if(array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n] - array[i])){
40                 return false;
41             }
42         }
43         return true;
44     }
45 
46     /**
47      * 打印数组值
48      * @param array
49      */
50     public static void print(int array[]){
51         for (int i = 0; i <max; i++) {
52             System.out.print(array[i]+" ");
53         }
54         count ++ ;
55         System.out.println();
56 
57     }
58 }

  代码分析

  首先我们定义了一个8个元素的一维数组 array ,用来表示一个8*8的棋盘。

  1)先来看下判断皇后是否与前面冲突(即在同一列或同一斜线)的judge方法:

    if(array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n] - array[i]))

    第一个条件array[i] == array[n],因一维数组的值即代表所在行的所在列值,所以如果值相同,则代表在同一列。

    第二个条件Math.abs(n-i) == Math.abs(array[n] - array[i]),n-i表示两个皇后相差几行,array[n]-array[i]表示相差几列,如果相差行等于相差,则这两个皇后能构成一个正方形,即在同一斜线上

  2)在来看执行判断过程的check方法:

 1 private static void check(int n){
 2         if(n == max){
 3             print(array);
 4             return;
 5         }
 6         for(int i=0; i<max; i++){
 7             array[n] = i;
 8             if(judge(n)){
 9                 check(n+1);
10             }
11         }
12     }

  第2行的if()条件判断,用于表示一次求解过程的结束。当n==max即n=8时,即表示前面已经放置了8个皇后(n从0开始)。

  第6行的for循环,表示从第0行的第0列开始放第一个皇后,一直到第0行的第7列遍历出所有第0行的皇后摆放方法。同理,执行到n=1时,表示放置第二个皇后,即第2行的摆放方法,只要第二行不跟第一行冲突,就在第三行放置第3个皇后,以此类推直到放置第7行的第八个皇后。如果在某行遍历完所在行的所有列,均与前面的皇后冲突,说明前面的摆放不能求解出一个八皇后解法,此时该行的循环执行结束,该行所在的方法出栈,回溯到前面一行的方法执行。前面一行继续执行for循环的i++,当i++后即该行皇后向后一个位置移动,如果不跟前面的所有皇后冲突,则再进入下一行的下一个皇后从第0列开始摆放,依此类推。

  当得到一个正确解法后,n=8所在方法出栈(参考前面讲解的递归方法入栈出栈),执行n=7(第8个皇后)所在方法的for循环,继续执行i++,查看最后一行的皇后后面列是否还有正确解法,如果有则输出,如果没有则该行所在方法出栈,进而执行n=6(第7个皇后)所在方法的for循环,继续执行i++。依此类推

  用文字描述稍微有点抽象,不过如果理解了我们前面讲解的递归方法的执行过程,理解起来还是比较容易的。这里使用了for循环求解八皇后的所有解法,所以相对会难以理解。

 图解的方式理解八皇后解法(虚线圆表示不能摆放,用实线圆形表示可以摆放

  在main方法中调用check(0)后,n=0的check方法入栈,并执行for循环的i=0,array[0]=0,即第一个皇后摆放在第0行第0列,此时程序栈和棋盘情况如下图所示  

  

 

  由于这时是第一个皇后,所以肯定没有冲突,但要记住n=0时的check方法的for循环只进行到i=0,便调用check(1),调用下一个皇后的摆放判断,此时程序栈和棋盘情况如下图所示

 

  

 

  当n=1的check方法入栈后,执行for循环方法,由于i=0和i=1均会与第一个皇后冲突,所以这两个位置不能摆放,此时n=1的check方法的for循环执行到i=2。第二个皇后摆放后,会调用check(2),则n=2的check方法入栈,此时程序栈和棋盘情况如下图所示

  

 

  第n=2的check方法入栈后,执行for循环方法,在i<4之前的所有位置均会与前面两个皇后冲突,所以只能放在i=4的位置。此时n=2的check方法的for循环执行到i=4。调用check(3),则n=3的check方法入栈,此时程序栈和期盼情况如下图所示

  

 

  n=3的所在行摆放皇后之后,调用check(4)的方法,此时n=2的check方法的for循环执行到i=4。

  依此类推,直到执行到n=5时,for循环执行完所有遍历,发现均与前面的皇后冲突,如下图所示

  

 

  当n=5的for循环执行完后,check(5)方法出栈,回溯到check(4)的方法继续执行for循环,前面我们知道check(4)的for循环i执行到i=3,所以从i=3继续执行i++,如下图所示

  

 

  可以看出n=4的check方法执行到i=7时,才能满足不与前面的皇后冲突,这时会继续调用check(5)方法,即n=5的check方法再次入栈,如下图所示

  

 

  可以看出n=5时,所在行的所有列均无法摆放皇后,所示n=5的check方法再次出栈,而n=4的check方法的for循环也执行到i=7,所以check(4)方法也会出栈,进而执行n=3的for循环,而我们之前记录可以看到n=3的for循环执行到i=1,所以继续执行i++,并依此判断是否与前面的皇后冲突。

  从上面的过程我们可以看到,当栈顶方法所在行的所有列均不能摆放皇后时,会回溯到前面的行执行。

  下面我们在用一个摆放成功的案例来讲解回溯过程,例如,0 4 7 5 2 6 1 3  即(0,0) (1,4) (2,7) (3,5) (4,2) (5,6) (7,1) (8,3)的摆法,此时程序栈和棋盘如下图所示

  

 

  可以看到,n=7时第八个皇后摆放成功,会调用check(8),进而满足if(n==8)条件,所以check(8)方法出栈,继续执行n=7的check方法,而此时n=7的for循环i=3,继续执行i++,看n=7的所在行的后面的列是否还有能摆放成功的。如果没有则n=7的check方法执行完毕,回溯到n=6的方法,依此类推,知道所有的八皇后解法全部求出。

 

总结

  好了,到这里不知道大家是否理解了使用递归回溯法求八皇后解法的问题?如有疑问的地方,可以在留言区评论提问。

 

posted @ 2019-10-05 16:28  听风1108  阅读(1244)  评论(1编辑  收藏  举报