【八皇后問題】算法
問題: 國際象棋棋盤是8 * 8的方格,每一個方格里放一個棋子。皇后這種棋子能夠攻擊同一行或者同一列或者斜線(左上左下右上右下四個方向)上的棋子。在一個棋盤上若是要放八個皇后,使得她們互相之間不能攻擊(即任意兩兩之間都不一樣行不一樣列不一樣斜線),求出一種(進一步的,全部)佈局方式。數組
■ 描述 & 實現數據結構
以前的Python基礎那本書上介紹遞歸和生成器的一張有解過這個問題。書本中對於此問題的解可能更偏重於對於Python語言的應用。然而果真我也是早就忘光了。下面再來從頭看看這個問題。函數
首先,咱們想到遞歸和非遞歸兩類算法來解決這個問題。首先說說遞歸地算法。佈局
很天然的,咱們能夠基於行來作判斷標準。八個皇后都不一樣行這是確定的,也就說每行有且僅有一個皇后,問題就在於皇后要放在哪一個列。固然八個列下標也都不能有相同,除此以外還要保證斜線上不能有重疊的皇后。優化
第一個須要解決的小問題就是,如何用數學的語言來表述斜線上重疊的皇后。其實咱們能夠看到,對於位於(i,j)位置的皇后而言,其四個方向斜線上的格子下標分別是 (i-n,j+n), (i-n,j-n), (i+n,j-n), (i+n,j+n)。固然i和j的±n都要在[0,7]的範圍內,保持不越界。暫時拋開越界限制無論,這個關係其實就是: 目標格子(a,b)和本格子(i,j)在同一條斜線上 等價於 |a - i| == |b - j| 。spa
而後,從遞歸的思想來看,咱們在從第一行開始給每一行的皇后肯定一個位置。每來到新的一行時,對本行的全部可能位置(皇后放在這個位置和前面全部已放置的皇后無衝突)分別進行遞歸地深刻;若某一行可能的位置數爲0,則代表這是一條死路,返回上一層遞歸尋找其餘辦法;若來到的這一行是第九行(不存在第九行,只不過是說明前八行都已經正確配置,已經獲得一個解決方案),這說明獲得解決方案。設計
能夠看到,尋找一行內皇后應該擺放的位置這是個遞歸過程,而且在進入遞歸時,應該要告訴這個過程的東西包括兩個: 1. 以前皇后放置的狀態, 2. 如今是第幾行。code
因此,遞歸主體函數能夠設計爲 EightQueen(board, row),其中board表示的是當前棋盤的狀態(好比一個二維數組,0表示未放置,1表示放有皇后的狀態)。另外還能夠有一個check(board,pos),pos能夠是一個(x,y)元組,check函數用來返回以當前的board棋盤狀態,若是在pos再放置一個皇后是否會有衝突。blog
基於上面的想法,初步實現以下:
def check(board,pos): # check函數暫時先不實現 pass def EightQueen(board,row): blen = len(board) if row == blen: # 來到不存在的第九行了 print board return True # 必定要return一個True,理由在下面 for possibleY in range(blen): if check(board,(row,possibleY)): board[row][possibleY] = 1 # 放置一個Queen if not EightQueen(board,row+1): # 這裏實際上是本行下面全部行放置皇后的遞納入口。可是若是最終這條路沒有找到一個解,那麼 # 此時應該將剛纔放置的皇后收回,再去尋找下一個可能的解 board[row][possibleY] = 0 else: return True return False
最開始,可能在迴歸返回條件那裏面不會想到要return True,而只是return。對應的,下面主循環中放置完Queen以後也只是簡單地遞歸調用EightQueen,不會作邏輯判斷。可是很快能夠發現這樣作存在一個問題,即當某一層遞歸中for possibleY這個循環走完卻沒有找到一個合適的解(即本行無合適位置),此時返回上一行,上一行的possibleY右移一格,此時以前放在這一行的Queen的位置仍然是1。這樣以後本行的全部check確定都是通不過的。因此咱們須要設計一個機制,使得第一個possibleY沒有找到合理的最終解決方案(這裏就加上了一個判斷條件),要右移一格到下一個possibleY時將本格的Queen收回。
這個判斷條件就是若是某層遞歸for possibleY循環整個走完未找到結果返回False(EightQueen整個函數最後的返回),上一層根據這個False反饋把前一個Queen拿掉;若是找到了某個結果那麼就能夠一路return True回來,結束函數的運行。
另外,若是隻是獲取一個解的話,能夠考慮在if row == blen的時候,打印出board,而後直接sys.exit(0)。此時就只須要for possibleY循環完了以後return一個False就能夠了。固然主循環中對於遞歸的返回的判斷 if not EightQueen仍是須要的。
■ 優化
● check函數怎麼搞
上面沒有實現check函數。其實仔細想一下,若是按照上面的設想來實現check函數仍是有點困難的。好比令 x,y = pos,儘管此時咱們只須要去檢查那些行下標小於x的board中的行,可是對於每一行中咱們仍是要一個個去遍歷,找到相關行中值是1的那個格子(忽然發現這個是one-hot模式誒哈哈),而後將它再和x,y這個位置作衝突判斷。因此可是這個check函數複雜度就可能會達到O(n^2),再套上外面的循環,複雜度蹭蹭往上漲。下面是check函數的一個可能的實現:
def check(board,pos): x,y = pos blen = len(board) for i in range(x): for j in range(blen): if board[i][j] == 1: if j == y or abs(j-y) == abs(i-x): return False return True
其實能夠看到,咱們花了一層循環在尋找某行中的one-hot,那些大量的0值元素是咱們根本不關心的。換句話說,對於board這個二維數組,其實咱們真正關心的是每行中one-hot值的下標值。天然咱們就能夠想到,能不能將board轉化爲一個一維數組,下標自己就表明了board中的某一行,而後值是指這一行中皇后放在第幾列。
若是是這樣的話,那麼程序就須要改造,首先是check函數要根據新的board數據結構作一些調整:
def check(board,row,col): i = 0 while i < row: if abs(col-board[i]) in (0,abs(row-i)): return False i += 1 return True
能夠看到,改變二維數組board變爲一維數組以後,咱們能夠在O(1)的時間就肯定row行以前每一行擺放的位置,並將其做爲參考進行每一行的衝突判斷。
而後是主函數的修改:
def EightQueen(board,row): blen = len(board) if row == blen: # 來到不存在的第九行了 print board return True col = 0 while col < blen: if check(board,row,col): board[row] = col if EightQueen(board,row+1): return True col += 1 return False def printBoard(board): '''爲了更友好地展現結果 方便觀察''' import sys for i,col in enumerate(board): sys.stdout.write('□ ' * col + '■ ' + '□ ' * (len(board) - 1 - col)) print ''
總的結構,和沒修改以前是相似的,只不過在主循環中,從上面的possibleY做爲遊標去設置 - 去除 一個位置的放置狀態,這種方式改成了簡單的col += 1。改爲col+=1的好處就是當某輪遞歸以失敗了結,返回上層遞歸以後,就不用再去特意收回以前放置好的Queen,而是能夠直接讓col += 1,。
printBoard函數能夠將一維數組的board狀態很直觀地展示出來:
■ □ □ □ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ ■ □ □ ■ □ □ □ □ □ □ □ □ □ ■ □ □ □ □
■ 全部結果?
上面的程序多隻是生成了一個結果,而實際上八皇后能夠有不少種可能的佈局。如何才能求得全部結果?其實只要小小地修改一下上面的程序就能夠了。
以上面修改事後一維數組維護棋盤狀態爲例。程序在碰到一次row == blen的狀況以後就返回了True,而後遞歸一層層地返回True直到最上層。因此找到一個解決方案以後,程序就會退出了。
反過來,若是得到一個解決方案以後,不判斷EightQueen函數的返回,此時函數會繼續執行col += 1,將狀態搜尋繼續下去,如此收集狀態的任務在row == blen的判斷中,(注意這裏的return可不能刪,這裏須要一個return來提示遞歸的終結條件),而對於每條遞歸路徑老是窮盡全部可能再回頭,這樣就能夠得到到全部可能了:
def EightQueen(board,row): blen = len(board) if row == blen: # 來到不存在的第九行了 print board return True col = 0 while col < blen: if check(board,row,col): board[row] = col if EightQueen(board,row+1): # return True 去掉這裏便可,或者直接刪除掉整個判斷,只留下單一個EightQueen(board,row+1) pass col += 1 return False
示例結果:
[0, 4, 7, 5, 2, 6, 1, 3] [0, 5, 7, 2, 6, 3, 1, 4] [0, 6, 3, 5, 7, 1, 4, 2] [0, 6, 4, 7, 1, 3, 5, 2] [1, 3, 5, 7, 2, 0, 6, 4] [1, 4, 6, 0, 2, 7, 5, 3] [1, 4, 6, 3, 0, 7, 5, 2] [1, 5, 0, 6, 3, 7, 2, 4] [1, 5, 7, 2, 0, 3, 6, 4] …… 總共有92種佈局方案
■ 非遞歸
非遞歸解這個問題,很顯然是要去維護一個stack來保存一個路徑了。簡單來講,這個棧中維護的應該是「還沒有嘗試去探索的可能」,當我開始檢查一個特定的位置,若是檢查經過,那麼應該作的是首先將本位置右邊一格加入棧,而後再把下一行的第一個格子加入棧。注意前半個操做很容易被忽視,可是若是不將本位置右邊一格入棧,那麼若是基於本格有皇后的狀況進行的遞歸最終沒有返回一個結果的話,接下來就不知道往哪走了。若是使用了棧,那麼用於掃描棋盤的遊標就不用本身在循環裏+=1了,循環中游標的移動全權交給棧去維護。
代碼以下:
def EightQueen(board): blen = len(board) stack = Queue.LifoQueue() stack.put((0,0)) # 爲了自洽的初始化 while not stack.empty(): i,j = stack.get() if check(board,i,j): # 當檢查經過 board[i] = j # 別忘了放Queen if i >= blen - 1: print board # i到達最後一行代表已經有告終果 break else: if j < blen - 1: # 雖說要把本位置右邊一格入棧,可是若是本位置已是行末尾,那就不必了 stack.put((i,j+1)) stack.put((i+1,0)) # 下一行第一個位置入棧,準備開始下一行的掃描 elif j < blen - 1: stack.put((i,j+1)) # 對於未經過檢驗的狀況,天然右移一格便可
顯然,把break去掉就是求全部解了
■ 用C語言寫了一版
#include <stdio.h> static int board[8] = {}; int board_size = sizeof(board)/sizeof(int); int check(int *board,int row){ int i = 0; while(i < row){ if(board[i] == board[row] || row - i == board[row] - board[i] || row - i == board[i] - board[row]){ return 0; } i++; } // printf("board[%d]: %d\n",row,board[row]); return 1; } void print_board(int *board){ int i; int size = board_size; for(i=0;i<size;i++){ printf("%d,",board[i]); } printf("\n"); i = 0; while (i < size){ int j; for (j=0;j<size;j++){ if(j == board[i]){ printf("%s ","■ "); } else{ printf("%s ","□ "); } } printf("\n"); i++; } } int eight_queen(int *board,int row){ if (row == 8){ print_board(board); return 1; } board[row] = 0; while (1){ if (check(board,row) && eight_queen(board,row+1)){ return 1; } else{ if(++board[row] >= 8){ break; } } } return 0; } int main(){ eight_queen(board,0); // print_board(board); return 0; }