八皇后問題是十九世紀著名的數學家高斯1850年提出:在8X8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。
八皇后問題不是隻用於這個有點無聊的數學問題上,不然就沒什麼研究意義了。在景觀設計,道路佈局,城市規劃等方面有應用價值。比如佈置景觀時,要讓N個景觀互相不遮擋視線。
方案一 暴力法
最容易實現的,8層循環嵌套
當然OK,但它的複雜度驚人,O(nn),n爲行列數
爲了減少循環次數,針對該問題,最外層循環從第一列開始,向內層依次加1,最內層循環就從第八列開始了。
方案二 回溯法
八皇后問題是回溯法經典的例子
回溯法基本的思想是採用遞歸,並且分步解決問題
首先擺第一行的皇后(都是從第一列開始擺)
然後擺第二行的皇后,擺在第一列,發現不行;擺第二列,也不行;擺第三列,行了。
接下來再擺第三行,都是從第一列開始擺,如果不行就依次往後挪。
以此類推,對於某一行,如果這一行每列都不行,那麼就得退回到上一行,(說明上一行的位置就已經不對了),上一行的皇后得往後再挪一格,如果已經是最後一格了,那就再退到上上一行……
如果擺到了最後一行,並且出現了一個解,那麼將這個解輸出後,同樣再退回到上一行,將上一行的皇后往後挪一格……看看還有沒有其他解,直到不可能再出現其他解爲止。
棋盤信息,爲了減少每次棋子調整帶來的cost,我們可以用四個一維數組來表示。(傳統上用一個二維數組表示的話,每次棋子調整要改變數組中很多個元素的值,而且相當混亂)
因爲每個皇后的控制範圍爲一行、一列再加對角線,所以與之對應的,我們把棋盤分成8行、8列、15個左下斜對角線和15個右下斜對角線,每一項都用一個一維數組表示。每次擺一個皇后的時候,只需改變將與之對應的行、列、左下斜對角線和右下斜對角線數組中元素(說明此行、列、對角線上不能再放其他皇后了),也就是隻需改變4個元素,而且這4個元素的在它們的數組中的下標與該皇后的橫縱座標有關(具體可以自己推算一下)。
類聲明如下(cpp)
class ChessBoard
{
public:
ChessBoard();
ChessBoard(int);
void findSolutions();
private:
const bool available; //表示行、列或對角線是否可用
const int squares, norm; //squares爲行數,norm爲squares-1(用於推算對角線的下標)
bool *column, *leftDiagonal, *rightDiagonal;
int *positionInRow; //記錄每一行皇后的下標(列號)
int howMany; //已放好的皇后的個數
void putQueen(int);
void printBoard(ostream&);
void initializeBoard();
};
關鍵方法的實現
void ChessBoard::putQueen(int row)
{
int col;
//對於每一行,都從第一格開始嘗試擺
for(col=0; col<squares; col++)
{
if(column[col]==available && leftDiagonal[row+col]==available && rightDiagonal[row-col+norm]==available) //該格子可用
{
positionInRow[row]=col;
column[col]=!available;
leftDiagonal[row+col]=!available;
rightDiagonal[row-col+norm]=!available;
if(row<squares-1)
putQueen(row+1); //進入下一行
else
printBoard(cout); //已找到一種解
//由於下一行無法滿足條件或者已找到一種解而退回來時
//恢復原來的狀態,continue
column[col]=available;
leftDiagonal[row+col]=available;
rightDiagonal[row-col+norm]=available;
}
}
}
其實用回溯法的話,也窮盡了每一種情況,也是exhaustive search,而且它的效率還不如暴力法,因爲用了遞歸。
方案三 Best-First Search
由於我們最初的問題是求所有的擺法,所以不管怎樣它一定是一個exhaustive search問題了。如果我們把問題改一下,對任意一種棋盤狀態(八行裏每行都只有一個皇后),怎樣才能最快的調整爲一個滿足條件的狀態。那這樣的話,就不能再像之前那樣盲目的去搜索了,得采用一定的策略了,就是Informed Search。Best-First Search、A*、Hill Climbing和Tabu Search等都能很好的解決該問題,我們以最基礎的Best-First Search來講述。
爲了簡化問題,我們先來看一個4X4的棋盤。
給定一個初始狀態,我們每次都對它挪動一個棋子(也就是擴展後繼節點)。第一張圖中,第一行的棋子能朝左或右挪一格,第二行只能朝右挪一格,第三行第四行都能朝左或右挪一格。圖中h表示深度,也就是調整的次數;g表示當前的Conflicts數,就是不符合的行、列和對角線總數。(圖中可能h和g畫反了,真正的h應該代表當前節點離目標節點的cost,也就是圖中的g;真正的g應該代表初始節點到當前節點的cost,也就是圖中的h)
根據Best-First Search算法,每次都將當前最好節點的所有後繼節點都插入OPEN List中,每次都選擇OPEN list中最好的節點進行擴展,直到選出的節點即爲目標節點(圖中的g=0)。每次都沿着當前最好的節點延伸,很快就能找到目標節點。