1. 程式人生 > >C++遞迴法解決八皇后問題的超詳細解答

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的簡單陣列。

對右斜線而言,其資料結構也是一個數組,但是陣列的下標不能為負數。因此該陣列具有7個元素,但是考慮到表示式r-c得到的負值,因此給r-c統一加上一個常數(norm),從而避免陣列越界。

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:

希望對大家有幫助,純手打實在辛苦,各位看官別忘了留下贊或者評論,給博主攢點積分吧!!!麼麼噠!!