大家好,我是“Stephen·謝”,本文以古老的八皇后問題的文字解釋和代碼實現,將遞歸回溯的思想概念介紹給大家。
國際象棋中的皇后比中國象棋里的大車還厲害,皇后能橫向,縱向和斜向移動,在這三條線上的其他棋子都可以被吃掉。所謂八皇后問題就是:將八位皇后放在一張8x8的棋盤上,使得每位皇后都無法吃掉別的皇后,(即任意兩個皇后都不在同一條橫線,豎線和斜線上),問一共有多少種擺法。此問題是在1848年由棋手馬克思·貝瑟爾提出的,后面陸續有包括高斯等大數學家們給出自己的思考和解法,所以此問題不只是有年頭了,簡直比82年的拉菲還有年頭,我們今天不妨嘗嘗這老酒。
我們先舉例來理解一下這個問題的場景到底是什么樣子的,下面的綠色格子是一個皇后在棋盤上的“封鎖范圍”,其他的皇后不能放置在這些綠格子中:
我們再放入一個皇后,看一下兩個皇后的“封鎖范圍”(綠格子不能放):
如此繼續下去,能安放下一位皇后的位置越來越少,那么我們最終如何能安放完這8位皇后呢?
首先我們看一下特別暴力的方法:從8x8的格子里選8個格子,放皇后,然后測試是否滿足條件,若滿足則結果加1,否則換8個格子繼續試。很顯然,64選8,并不是個小數字,十億級別的次數,夠暴力。如果換成圍棋的棋盤,畫面就會太美而不敢算。
稍加分析,我們可以得到另一個不那么暴力的方法:顯然,每行每列最多只能有一位皇后,如果基于這個事實再進行暴力破解,那結果會好很多。安排皇后時,第一行有8種選法,一旦第一行選定,假設選為(1,i),那么第二行只能選(2,j),其中,j不等于i,所以有7種選法。以此類推,需要窮舉的情況有8!=40320種,比十億級別的小很多了。
這看起來已經不錯了,但嘗試的次數還是隨著問題規模按階乘水平提高的,我們仍然不滿意,所以,“遞歸回溯”的思想就被提出了,專治這種問題。
為了理解“遞歸回溯”的思想,我們不妨先將4位皇后打入冷宮,留下剩下的4位安排進4x4的格子中且不能互相打架,有多少種安排方法呢?如果按照上面方式窮舉,需要4!=24次嘗試嗎?
現在我們把第一個皇后放在第一個格子,被涂黑的地方是不能放皇后的:
第二行的皇后只能放在第三格或第四格,比如我們放在第三格:
這樣一來前面兩位皇后已經把第三行全部鎖死了,第三位皇后無論放在第三行的哪里都難逃被吃掉的厄運。于是在第一個皇后位于第一格,第二個皇后位于第三格的情況下此問題無解。所以我們只能返回上一步,來給2號皇后換個位置:
此時,第三個皇后只有一個位置可選。當第三個皇后占據第三行藍色空位時,第四行皇后無路可走,于是發生錯誤,則返回上層調整3號皇后,而3號皇后也別無可去,繼續返回上層調整2號皇后,而2號皇后已然無路可去,則再繼續返回上層調整1號皇后,于是1號皇后往后移一格位置如下,再繼續往下安排:
上面的圖例正是回溯遞歸思想的展現,然而知易行難,在代碼中我們怎樣來實現這種算法呢?實現的代碼有很多種,我們找一個最簡單的來舉例吧:
我們來重點看一下這段代碼(這段代碼雖短,但真的非常非常重要,是整個算法的核心和靈魂):
第一次進來,row=0,意思是要在第一行擺皇后,只要傳進來的row參數不是8,表明還沒出結果,就都不會走if里面的return,那么就進入到for循環里面,column從0開始,即第一列。此時第一行第一列肯定合乎要求(即check方法肯定通過),能放下皇后,因為還沒有任何其他皇后來干擾。
關鍵是check方法通過了之后,在if里面又會調用一下自己(即遞歸),row加了1,表示擺第二行的皇后了。第二行的皇后在走for循環的時候,分兩種情況,第一種情況:for循環沒走到頭時就有通過check方法的了,那么這樣就順理成章地往下走再調用一下自己(即再往下遞歸),row再加1(即擺第三行的皇后了,以此類推)。第二種情況:for循環走到頭了都沒有通過check方法的,說明第二行根本一個皇后都擺不了,也觸發不了遞歸,下面的第三行等等后面的就更不用提了,此時控制第一行皇后位置的for循環column加1,即第一行的皇后往后移一格,即擺在第一行第二列的位置上,然后再往下走,重復上述邏輯。
注意,一定要添加清零的代碼,它只有在皇后擺不下去的時候會執行清0的動作(避免臟數據干擾),如果皇后擺放很順利的話從頭到尾是不會走這個請0的動作的,因為已經提前走if里面的return方法結束了。
總之,這段核心代碼很繞,原理一定要想通,想個十幾二十遍差不多就能理解其中的原理了,遞歸回溯的思想也就不言而喻了。八皇后問題一共有92種情況,下面是用Java實現的完整代碼:
public static int[][] arry=new int[8][8];//棋盤,放皇后
public static int map=0;//存儲方案結果數量
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("八皇后問題");
findQueen(0);
System.out.println("八皇后問題共有:"+map+"種可能");
}
public static void findQueen(int i){//尋找皇后節點
if(i>7){//八皇后的解
map++;
print();//打印八皇后的解
return;
}
for(int m=0;m<8;m++){//深度回溯,遞歸算法
if(check(i,m)){//檢查皇后擺放是否合適
arry[i][m]=1;
findQueen(i+1);
arry[i][m]=0;//清零,以免回溯的時候出現臟數據
}
}
}
public static boolean check(int k,int j){//判斷節點是否合適
for(int i=0;i<8;i++){//檢查行列沖突
if(arry[i][j]==1){
return false;
}
}
for(int i=k-1,m=j-1; i>=0 && m>=0; i--,m--){//檢查左對角線
if(arry[i][m]==1){
return false;
}
}
for(int i=k-1,m=j+1; i>=0 && m<=7; i--,m++){//檢查右對角線
if(arry[i][m]==1){
return false;
}
}
return true;
}
public static void print(){//打印結果
System.out.print("方案"+map+":"+"\n");
for(int i=0;i<8;i++){
for(int m=0;m<8;m++){
if(arry[i][m]==1){
//System.out.print("皇后"+(i+1)+"在第"+i+"行,第"+m+"列\t");
System.out.print("o ");
}
else{
System.out.print("+ ");
}
}
System.out.println();
}
System.out.println();
}