八皇后問題,是一個古老而著名的問題,是回溯算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於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的方法,依此類推,知道全部的八皇后解法所有求出。
好了,到這裏不知道你們是否理解了使用遞歸回溯法求八皇后解法的問題?若有疑問的地方,能夠在留言區評論提問。