java遞歸求八皇后問題解法

八皇后問題

八皇后問題,是一個古老而著名的問題,是回溯算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。 高斯認爲有76種方案。1854年在柏林的象棋雜誌上不一樣的做者發表了40種不一樣的解,後來有人用圖論的方法解出92種結果。算法

網上有不少八皇后的小遊戲,不清楚規則的能夠點擊這裏體驗一把。數組

 

遞歸理解

因爲咱們使用經典的遞歸回溯算法,因此要先理解遞歸的調用過程,在使用遞歸前咱們先看下普通方法的調用過程在JVM中如何體現。首先咱們來看下jvm中五個重要的空間,以下圖所示jvm

這裏咱們主要關注棧區,當調用某個方法時會在棧區爲每一個線程分配獨立的棧空間,而每一個方法都會以棧幀的形式壓入棧中,即每一個方法從進入到退出都對應着一個棧幀的壓棧和出棧。以下圖所示spa

 

在每一個棧幀中都會有方法獨立的局部變量表,操做數棧,動態鏈接,返回地址等信息。以下圖所示線程

 

理解了jvm程序棧的結構後,下面咱們以圖解的方式先講解一下普通方法(理解普通方法調用過程後,再講解遞歸的調用過程)的調用過程。3d

假設程序main方法首先調用了method1,在method1中調用了method2,在method2中調用method3。代碼以下code

 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方法時,會執行如下步驟blog

  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的方法,依此類推,知道全部的八皇后解法所有求出。

 

總結

  好了,到這裏不知道你們是否理解了使用遞歸回溯法求八皇后解法的問題?若有疑問的地方,能夠在留言區評論提問。

相關文章
相關標籤/搜索