不說廢話,看題目:java
如何可以在 8×8 的國際象棋棋盤上放置八個皇后,使得任何一個皇后都沒法直接吃掉其餘的皇后?爲了達到此目的,任兩個皇后都不能處於同一條橫行、縱行或斜線上。編程
咱們能夠將它看做一個搜索問題:數組
(1)以行爲單位,逐行向下搜索。app
(2)每一行都應該搜索全部可能的值。jvm
(3)以前行的皇后的擺放位置會影響後面行皇后的擺放位置。函數
注意第三點,由於咱們每擺放一個皇后,都會由於新皇后的做用域而改變整個棋盤的格局,從而影響後續皇后的擺放。因此咱們在搜索時,一旦一種路徑已經搜索完成,咱們須要將嘗試該路徑時擺放的皇后清空。工具
那麼讓咱們嘗試將每一行須要作的事情寫出來:this
遍歷該行的每個格子,對每個格子作下面的事:spa
判斷該格子是否能夠擺放皇后。3d
若是則在該格子擺放皇后,並以此爲基礎遞歸搜索下面行全部可能的擺放組合。
若是不能夠擺放皇后,什麼都不作。
這個格子嘗試完了,清空該格子狀態,繼續for循環嘗試本行其它格子。
其中「若是能夠擺放皇后,搜索該格子擺放皇后時下面行全部可能的擺放組合。」是一個明顯的遞歸調用,「這個格子嘗試完了,清空該格子狀態,繼續for循環嘗試本行其它格子。」是一個明顯的回溯點。咱們將上述描述轉化爲代碼:
其實到了這裏,實現就只剩下如「如何判斷該格子是否能夠擺放皇后」等細節問題了。
另外有些朋友可能對回溯的過程有疑問,對遞歸調用的過程理解的不是很清晰。建議看一下函數棧的結構以及函數是如何運行的,遞歸函數也是一個普通函數,只是本身調用了本身。執行時無非也是臨時變量、寄存器值、ebp、pc指針的壓棧彈棧。函數對於底層來講就是一段指令,遞歸函數是底層在重複的執行這一段指令,但每次執行時存的臨時變量不一樣罷了。函數內部調用另外一個函數,保存完自己的執行環境後pc指針指向被調用函數併爲被調用函數開闢新的棧幀,遞歸調用就只是pc指針又指向了本身併爲本身又開闢了一個新的棧幀,與其它函數並無什麼不一樣。
要說不一樣的話,對於普通的函數調用咱們是知道或者說能夠控制須要調用多少層級的,也就是說咱們須要的棧大小是有限的。而遞歸調用時,除了本題這種咱們知道調用層級層數的遞歸(本題8層),其它形式的遞歸只有到達迴歸條件纔會終止調用。咱們並不知道到達迴歸條件時函數調用了多少層,若是層級過深極有可能會致使棧過大而引發內存不足。
另外開闢新的棧幀以及保存和恢復函數執行現場是很是消耗時間的,這就致使了遞歸函數的效率明顯低於遞推函數,由於要頻繁的進行函數調用。在非用遞歸不可的時候(還沒碰到過)能夠嘗試一下將函數定義爲final(java),jvm會嘗試將final函數在編譯期進行內聯,以減小函數調用的發生。
因此在生產環境中咱們應該儘可能避免這種不可控的寫法,而應該用尾遞歸或將遞歸轉化爲遞推的方式來實現功能。遞歸只是咱們思考的工具,遞歸的代碼寫出來咱們也就對問題的解空間有了一個總體的認識,想象一下遞歸的迴歸過程即是遞推的過程。
下面放出N皇后完整代碼(java版,棋盤大小設爲8即爲8皇后),有興趣的盆友能夠跑一下:
棋盤類:
package learning; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; public class Chessboard { //棋盤數組 private int[][] chessboard; //棋盤寬度 private int x; //棋盤高度 private int y; public int[][] getChessboard(){ return this.chessboard; } //構造棋盤 public Chessboard(int x, int y) { if (x <= 0 || y <= 0) { throw new RuntimeException("棋盤大小必須爲正整數"); } this.x = x; this.y = y; chessboard = new int[x][y]; } public int getX(){ return this.x; } public int getY(){ return this.y; } public void setQueen(int x,int y){ this.chessboard[x][y]=1; } //清除x如下行的棋子 public void clean(int x) { if (x < 0 || x > this.x - 1) { throw new RuntimeException("行號不合法"); } for (int i = x ; i < this.x; i++) { for (int j = 0; j < this.y; j++) { chessboard[i][j] = 0; } } } //打印棋盤 public String print() { if (x <= 0 || y <= 0) { throw new RuntimeException("棋盤大小必須爲正整數"); } System.out.println("該棋盤爲------>"); for (int i = 0; i < x; i++) { StringBuffer sb = new StringBuffer(); for (int j = 0; j < y; j++) { sb.append(chessboard[i][j]); if (j != y - 1) { sb.append(","); } } System.out.println(sb.toString().trim()); } return null; } //查看該格子可不能夠放置皇后 public boolean canSetQueen(int x, int y) { if (x < 0 || y < 0 || x > this.x - 1 || y > this.y - 1) { throw new RuntimeException("格子不在棋盤內!"); } //判斷同一列 for (int i = 0; i < this.x; i++) { if (chessboard[i][y] != 0 && i != x) { return false; } } //判斷同一行 for (int j = 0; j < this.y - 1; j++) { if (chessboard[x][j] != 0 && j != y) { return false; } } //判斷斜線 for (int i = 0; i < this.x; i++) { for (int j = 0; j < this.y; j++) { if (x - i == y - j || i - x == j - y || i - x == j - y || i - x == y - j) { if (chessboard[i][j] != 0) { return false; } } } } return true; } }
主類:
package learning; public class NQueeen { static int num=0; public static void main(String[] args){ Chessboard chessboard=new Chessboard(8,8); setQueens(chessboard,0); System.out.println("共有 :"+num+" 種擺放方式"); } /** * @param chessboard 棋盤對象 * @param x 當前處理行數 * @Discreption N皇后回溯核心方法 */ public final static void setQueens(Chessboard chessboard,int x){ //判斷一下行號是否合法 if(x<0||x>chessboard.getX()){ throw new RuntimeException("x is to large than chessboard!"); } //迴歸條件,搜索到了最後一行 if(x>=chessboard.getX()){ //N皇后擺放完畢,打印棋盤 chessboard.print(); num++; return; } //j爲當前行的格子索引 for(int j=0;j<chessboard.getY();j++){ //判斷當前格子是否能夠擺放皇后 if(chessboard.canSetQueen(x,j)){ //擺放皇后 chessboard.setQueen(x,j); //遞歸搜索下一行全部的可能 setQueens(chessboard,x+1); } //當前行的當前格子嘗試完畢,清除該格子中的皇后,經過for循環嘗試該行其它格子 int[][] iii=chessboard.getChessboard(); iii[x][j]=0; } } }
END;
@Author 牛有肉 轉載請註明出處