C++遞迴法解決八皇后問題的超詳細解答
博主初學C++資料結構與演算法(清華大學出版社)第四版,由於程式清單5-2沒有詳細解答且程式碼不完整,思考了一個早上才恍然大悟,深感自己閱讀程式碼以及寫程式碼能力的不足,並在此記錄,同時也希望也能幫到有需要的人!
1、什麼是八皇后問題?
在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。例如下左圖所示:
可見,每個皇后所處的位置,不在其他皇后的同行,同列,以及同一斜線上。
2、解決思路
1)先考慮四皇后,即在4*4的棋盤格上放置4皇后,滿足上述規則,進而在拓展至8皇后。
2)我們一把4*4棋盤格的行列定義如下:
3)最自然的實現方法是宣告一個表示棋盤的4×4陣列 board,其元素是0和1。1代表此位置可以放置皇后,而0則表示不可以。這個陣列初始化為1,每當把一個皇后放在位置(r,c), board[r][c]就設定為0。同時,函式將所有不能放置棋子的位置均設定為0,即第r行和第c列上的所有位置,以及與(r,c)在同一條斜線上的所有位置。
4)上圖給出了4×4棋盤。注意圖上指示“左”的斜線上所有位置的橫豎座標加起來為2,r+c=2這個數字與這條對角線相關。一共有7條左斜線,與它們相關的數分別是0到6。圖上指示“右”的斜線上所有位置的橫豎座標之間的差值相同,r-c=-1,每條右斜線的這個值都不同。這樣,給右斜線賦值為-3到3。左斜線使用的資料結構是一個下標從0到6的簡單陣列。
3、程式碼實現
(建議大家把程式碼複製到VS中,運用快捷鍵方便理解程式碼:例如F12為轉到定義,Alt+F12為速覽定義)
#include <iostream> using namespace std; class ChessBoard { public: ChessBoard(); // 8 x 8 chessboard;自定義建構函式 ChessBoard(int); // n x n chessboard;帶有引數的建構函式 void findSolutions(); private: const bool available; const int squares, norm;//squares代表棋盤格的邊長,norm的意義在2、(4)中有提到 bool *column, *leftDiagonal, *rightDiagonal;//定義列,左斜線以及右斜線 int *positionInRow, howMany;//定義行以及方法的數量 char m[10][10];//記錄棋盤格 void putQueen(int); void printBoard(); void initializeBoard(); void Delete();//釋放new分配的動態記憶體 }; ChessBoard::ChessBoard() : available(true), squares(8), norm(squares - 1) { initializeBoard(); } ChessBoard::ChessBoard(int n) : available(true), squares(n), norm(squares - 1) { initializeBoard(); } void ChessBoard::initializeBoard() { register int i;//將整數i暫存器,目的使的運算更快 column = new bool[squares]; positionInRow = new int[squares]; leftDiagonal = new bool[squares * 2 - 1];//左斜線的數目 rightDiagonal = new bool[squares * 2 - 1];//右斜線的數目 for (i = 0; i < squares; i++) positionInRow[i] = -1;//positionInRow是一個數組,i,即下標代表其行數, //positionInRow[i]儲存的值為其列數 for (i = 0; i < squares; i++) column[i] = available;//將每一列都設定為可以放置皇后的情況 for (i = 0; i < squares * 2 - 1; i++) leftDiagonal[i] = rightDiagonal[i] = available; howMany = 0; } void ChessBoard::printBoard() { howMany++;// cout << howMany << " way is:" << endl; //為棋盤格賦值為1 for (int i = 0;i != squares;i++) { for (int j = 0;j != squares;j++) m[i][j] = '1'; } //將皇后的位置在棋盤格上用'*'標誌出來 for (int row = 0;row != squares;row++) m[row][positionInRow[row]] = '*'; //列印棋盤格 for (int i = 0;i != squares;i++) { for (int j = 0;j != squares;j++) cout << m[i][j]; cout << endl; } cout << endl; } //具體見部落格內容 void ChessBoard::putQueen(int row) { for (int 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(); column[col] = available; leftDiagonal[row + col] = available; rightDiagonal[row - col + norm] = available; } } } void ChessBoard::Delete() { delete[]column; delete[]positionInRow; delete[]leftDiagonal; delete[]rightDiagonal; } void ChessBoard::findSolutions() { putQueen(0); cout << howMany << " solutions found.\n"; Delete(); } int main() { ChessBoard board(7); board.findSolutions(); while (true) { } return 0; }
4、程式碼詳細解讀
1)這裡重點介紹putQueen成員函式,其餘函式相信大家在程式碼的註釋可以看懂,不懂的可以在評論區回覆我或者私信我,我會第一時間給大家答覆。
2)putQueen成員函式用了遞迴的方法。下面先給大家大概講解一下遞迴演算法。不知道大家有沒有看過《盜夢空間》,個人感覺遞迴的演算法就好像《盜夢空間》裡進入夢境一樣。正如下面的程式碼:
#include <iostream>
using namespace std;
double power(double x, unsigned int n) {
if (n == 0)
return 1.0;
else
return x * power(x, n - 1);
}
int main(){
cout << power(5,3) << endl;
while (true) {}
return 0;
}
這段程式碼主要作用是計算x的n次冪。當要計算5^3時,函式power則會執行return x * power(x, n - 1);這個函式式。
1)我們可以把power這個函式式想成是現實中在睡覺做夢一般(第一層夢境),然後再呼叫return x * power(x, n - 1);的位置,就好像是進入了下一層夢境一般(第二層夢境),來到了power(5,2);
2)在power(5,2)裡(第二層夢境),此後又會再一次呼叫return 5 * power(5, 2 - 1),在這個位置,又進入了下一層夢境(第三層夢境)。
3)然而在這一層夢境中(第三層夢境)(power(5, 2 - 1)),我們找到了我們想要找到的東西,就是此時n=1,函式返回1.0,這個的意思就是power(5, 1)=1。找到了我們想要找的東西后,我們必須原路返回,不然就會被困在夢境中,不能脫身。
4)此時我們將按照進來的位置原路返回到第二層夢境,即power(5,2)的return語句,在這裡,我們將找到的東西power(5, 2-1)=1代入return 5 * power(5, 2 - 1),得出power(5,2)=5;
5)此後我們返回進入第二層夢境的地方,即第一層的return x * power(5, 3-1);的位置,power(5,2)=5代入便可找到最終解,power(5,3)返回125。
遞迴的好處就是程式碼看上去更直觀一些,邏輯上的簡單性以及可讀性,其代價是降低了運算速度,這涉及到函式呼叫時棧幀的相關知識,在此不做過多討論。
言歸正傳,回到putQueen的程式碼
void ChessBoard::putQueen(int row) {
for (int 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();
column[col] = available;
leftDiagonal[row + col] = available;
rightDiagonal[row - col + norm] = available;
}
}
}
0、先說明一下:row=0代表棋盤格的第一行,col=0時代表棋盤格的第一列,即[0,0]為第一行第一列。
1、首先col=0,row0=0,由於我們在initializeBoard函式裡將column、leftDiagonal、rightDiagonal權初始為1,即可以放置皇后。
2、進入if結構,用positionInRow[0]記錄下此時的列數,說明皇后放置在[0,0],將第一列以及其斜線設定為不可放置狀態。正如在{2、解決思路中的(4)}所述,此點的右斜線上的位置的r-c是常數,加上常數norm確保下標不為負數;此點的左斜線上的位置的r+c為常數,以此來確保位置的唯一性。
3、此後,由於row<square-1,即沒有到達邊界,則進入遞迴,在這個位置從第一夢境進入到第二夢境,即putQueen(0+1)。
4、在第二夢境中,此時又是從col=0開始,但此時row=1,即皇后要在第二行找位置,此時由於在[0,0]的位置已經放置了皇后了,所以此時的column[0],leftDiagonal[0],rightDiagonal[3]都是不可訪問的,這限制了此時皇后的col不能等於0,或1,在迴圈的作用下即等於2,如下圖所示
5、同(2、),記錄下此時的列數,設定不可放置狀態。此時狀態圖為
6、同(3、)由於row<square-1,即沒有到達邊界,則進入遞迴,在這個位置從第二夢境進入到第三夢境(即putQueen(1+1))。
7、由上圖可見,皇后只能放置在[2,1]處,而後記錄下此時的列數,設定不可放置狀態,進入第四夢境(即putQueen(2+1))。
8、但是此時,我們可以得知,通過for迴圈是進入不了if條件裡面的語句的,即找不到這個點,那麼此時經過4次迴圈後,putQueen(2+1)執行完畢,但是此時函式將什麼也不做。現在我們可以得知第四層夢境已經結束了,我們要返回上一層夢境了,即putQueen(2),要注意,這層夢境的東西僅與這層夢境相關,不與上一層或者說下一層相關,就好比在putQueen(2)裡,row就等於2,其他引數也是如此。
9、返回至第三夢境,putQueen(2),則進行的是如下步驟:
column[col] = available;
leftDiagonal[row + col] = available;
rightDiagonal[row - col + norm] = available;
這些步驟的目的將作用於我們在第三層裡設定的不可放置狀態,將他們全部設定為可放置狀態。因為要是成功的話,其實此時row應該等於3,呼叫else裡面的printBoard()函式。要是沒有在呼叫的話,只有兩種可能:一是下一層的嘗試失敗了,所以就要改變當層的放置情況,當然就要把已經設定為不可放置狀態的reset。這種情況就是我們現在遇到的狀況,可以在任一層夢境中實現。二是已經成功了,呼叫了else後,打印出了棋盤格,但將此次的不可放置狀態reset,是想著尋找更多的方法,因此也需要reset。
9、當上述步驟完成的時候,切記切記,你以為就直接返回第二夢境了嗎?大錯特錯!而是會在接著進行兩次for迴圈,狀態圖如下
兩次for迴圈分別為:一次col=2以及col=3的for迴圈,但我們都知道,這是不可能會被放置的位置的,所以當迴圈結束了,即putQueen(2)執行完畢,返回至第二夢境,即putQueen(1);
10、返回至putQueen(1)時,將此次的不可放置狀態reset,reset後進入col=3的迴圈,即在row=1時,將皇后放置在[1,3],狀態圖如下,各位聰明的寶寶們肯定知道這樣也是不行的,最終將會返回到第一層夢境
11、同理。reset,然後通過for迴圈將皇后放置在[0,1]上,然後進入下一層,通過for把皇后放在[1,3]上,[2,1]上,[3,3]上,就成功實現了第一種方法!如圖:
12、接下來則在第四層夢境中,將row=3時的情況reset,然後是先進行一次col=3的for迴圈再返回至第三層!!!同樣在第三層reset,然後在進行for迴圈嘗試所有的可能!!!(不可遺漏)
13、下面就是squares=4、5、6、8時的部分輸出
squares=4:
squares=5:
squares=6:
squares=8:
希望對大家有幫助,純手打實在辛苦,各位看官別忘了留下贊或者評論,給博主攢點積分吧!!!麼麼噠!!