······
不说废话,看题目:
如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。
我们可以将它看作一个搜索问题:
(1)以行为单位,逐行向下搜索。
(2)每一行都应该搜索所有可能的值。
(3)之前行的皇后的摆放位置会影响后面行皇后的摆放位置。
注意第三点,因为我们每摆放一个皇后,都会因为新皇后的作用域而改变整个棋盘的格局,从而影响后续皇后的摆放。所以我们在搜索时,一旦一种路径已经搜索完成,我们需要将尝试该路径时摆放的皇后清空。
那么让我们尝试将每一行需要做的事情写出来:
遍历该行的每一个格子,对每一个格子做下面的事:
判断该格子是否可以摆放皇后。
如果则在该格子摆放皇后,并以此为基础递归搜索下面行所有可能的摆放组合。
如果不可以摆放皇后,什么都不做。
这个格子尝试完了,清空该格子状态,继续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 牛有肉 转载请注明出处