八皇后問題一向用來作爲回溯的樣例,這裏就稍微拋下自己當初的解法
問題:在一個8*8的棋盤上擺放八個皇后,要求皇后不能相互攻擊。
解法:這裏以C語言爲求解語言,先講解非遞歸方法,之後講解遞歸方法
由於皇后攻擊方向爲橫向,豎向,和斜線攻擊,問題也就簡化爲每排擺放一個皇后,
也即找到每排擺放皇后的位置。
一個簡單的思路就是可以利用1個1維數組來求解,定義爲全局變量,方便調用。
int arr[8]; // 用於每排皇后的位置,0-7表示第1列到第8列,初始化爲-1,表示無皇后
所以一個遍歷即可完成。
for( int row = 0; row < 8; ++row )
{
// 這裏就是定位每排皇后的位置
}
那麼問題就直接集中在如何確定每排皇后的位置
由於每排皇后有8個擺放的位置,所以只能一個一個試,即
for( int col = 0; col < 8; ++col )
{
if( 判斷該排,該列是否可以擺放皇后 )
{
可以擺放,就直接確定皇后的位置,然後跳出循環
arr[row] = col;
break;
}
}
這裏的if判斷其實只用確定,第row排的皇后和之前的皇后是否衝突即可,
判斷也很簡單,只要不在同一列,而且斜線對角不成45度或135度即可,
所以可以抽離成一個函數單獨實現
// 用於判斷第row排第col列的皇后和前row排的皇后是否衝突
// 衝突返回true
bool judge(int row, int col)
{
for( int i = 0; i < row; ++i )
{
if( col == arr[i] || // 判斷是否位於同一列
fabs( i - row ) == fabs( arr[i] - col ) ) // 判斷是否位於斜線上
return true;
}
return false;
}
那麼上面的循環就很好寫了
for( int row = 0; row < 8; ++row )
{
for( int col = 0; col < 8; ++col )
{
if( !judge( row, col ) )
{
arr[row] = col;
break;
}
}
}
當然,做到這一步很簡單,但卻得不到正確的解,若是第row排每一列都不能擺放皇后,
則第row排產生不了結果
這裏就需要回溯判斷,
1、因爲第row排都不能擺放,那麼就必須修改row-1排的皇后位置,
2、然後再來擺放第row排的皇后,確定好位置,則繼續row+1排皇后的位置
3、如果繼續不能擺放,則重複1,若第row-1排的皇后所有的位置換過之後,
還是確定不了row排皇后的位置,那麼就繼續修改row-2排,直到可以,或者
已達到第0排,還是不行,則結束查找。
那麼做出以下修改,加粗爲增加部分
for( int row = 0; row < 8; ++row )
{
int col = 0; // col需要在for之外用到,因爲受作用域的限制,這裏需要提出來
// 這裏需要有些處理,稍下分析
for( ; col < 8; ++col )
{
if( !judge( row, col ) )
{
arr[row] = col;
break;
}
}
// 若是col == 8表示第row排皇后,已經嘗試過每一列,
// 但是均不能擺放,需要重新擺放第row-1排的皇后
if( col == 8 )
{
row -= 2; // 這裏需要-2,因爲for循環中有一個++row
}
}
這裏只是單純回到了row-1排,卻沒有任何處理,因爲第row-1排之前已經放過皇后了(假設爲第y列),
所以現在row-1排的皇后只需要從第y+1列開始擺放即可,所以可以添加的處理是
if( arr[row] != -1 ) // 如果這是回溯的一排,那麼必然已經擺放過皇后了,那就單獨處理
{
col = arr[row] + 1; // 既然是回溯的,就令該排皇后從擺放在y+1列處
arr[row] = -1; // 既然是回溯的,就表示這排重新擺放,所以就直接令該排爲-1,表示沒擺皇后
}
好了,處理到了,這一步,如果不考慮邊界問題,基本上已經可以實現輸出一種情況
(1,1) (2,5) (3,8) (4,6) (5,3) (6,7) (7,2) (8,4) #1
這種結果恰巧不涉及到回溯2排的情況,所以,正好可以輸出,現在添加邊界處理
if( arr[row] != -1 ) // 如果這是回溯的一排,那麼必然已經擺放過皇后了,那就單獨處理
{
col = arr[row] + 1; // 既然是回溯的,就令該排皇后從擺放在y+1列處
arr[row] = -1; // 既然是回溯的,就表示這排重新擺放,所以就直接令該排爲-1,表示沒擺皇后
// 添加邊界處理
if( arr[row] == 7 ) // 都到了最後一列也不行,需要繼續回溯
{
row -= 2;
continue;
}
}
到目前爲止,所以的處理都已經結束,處理了邊界,現在看下全貌
這裏去掉註釋和judge函數
void slove()
{
for( int row = 0; row < 8; ++row )
{
int col = 0;
if( arr[row] != -1 )
{
col = arr[row] + 1;
arr[row] = -1;
if( arr[row] == 8 )
{
row -= 2;
continue;
}
}
for( ; col < 8; ++col )
{
if( !judge( row, col ) )
{
arr[row] = col;
break;
}
}
if( col == 8 )
{
row -= 2;
}
}
}
好了,再接再厲,實現全部結果輸出
要實現全部輸出,則需要在完成一次結果輸出之後,繼續回溯上一排,以祈求一次新的輸出
這樣重複直到回溯到第1排第8列這最後一個結束爲止,即可退出查找新的擺放方法
那麼修改如下
void slove()
{
// 如果回溯到了第1排第8列,就退出
if( arr[row] == 7 && row == 0 )
{
return ;
}
// 和原先內容一樣
// 若是最後一排也擺放好皇后,則輸出該種擺法結果,然後繼續循環
if( arr[row] != -1 && row = 7 )
{
disp(); // 一個輸出函數
// 需要回溯到上一排,以祈求新的擺法
}
}
這裏由於使用非遞歸,要實現這種跳轉,就只能採用goto,儘管我們學習中,都被告誡限制使用goto,但是
在不破壞程序結構的同時,採用一下也是可以的
最終結果如下
void slove()
{
for( int row = 0; row < 8; ++row )
{
if( arr[row] == 7 && row == 0 )
{
return ;
}
label: // 跳到這裏,處理回溯的那一行
int col = 0;
if( arr[row] != -1 )
{
col = arr[row] + 1;
arr[row] = -1;
if( arr[row] == 8 )
{
row -= 2;
continue;
}
}
for( ; col < 8; ++col )
{
if( !judge( row, col ) )
{
arr[row] = col;
break;
}
}
if( col == 8 )
{
row -= 2;
}
if( arr[row] != -1 && row == 7 )
{
disp();
goto label;
}
}
}
至此,關於非遞歸的解法就全部結束了
這裏將此封裝下,給下可以運行的全套代碼
/*
* eighetPuzzle.cpp
*
* Created on : 2012-11-26
* Author : zwsatan
*/
#include <iostream>
#include <cmath>
using namespace std;
class PuzzleQueen
{
int arr[8];
bool judge(int row, int col)
{
for( int i = 0; i < row; ++i )
{
if( col == arr[i] || fabs( i - row ) == fabs( arr[i] - col ) )
return true;
}
return false;
}
public:
PuzzleQueen()
{
for( int i = 0; i < 8; ++i )
arr[i] = -1;
}
void slove()
{
for( int row = 0; row < 8; ++row )
{
if( arr[row] == 7 && row == 0 )
{
return ;
}
label:
int col = 0;
if( arr[row] != -1 )
{
col = arr[row] + 1;
arr[row] = -1;
if( arr[row] == 8 )
{
row -= 2;
continue;
}
}
for( ; col < 8; ++col )
{
if( !judge( row, col ) )
{
arr[row] = col;
break;
}
}
if( col == 8 )
{
row -= 2;
}
if( arr[row] != -1 && row == 7 )
{
disp();
goto label;
}
}
}
void disp()
{
static int count = 0;
for( int i = 0; i < 8; ++i )
{
cout << "(" << i+1 << "," << arr[i] + 1 << ") ";
}
cout << "#" << ++count;
cout << endl;
}
};
int main(int argc, char *argv[])
{
PuzzleQueen pq;
pq.slove();
return 0;
}
疲乏了嗎?
還是繼續接下來看遞歸的解法,`(*∩_∩*)′
當然,我們絕大多數時間,對待回溯採用的都是遞歸方法,因此,我們在上述的方法上,稍作修改,
以使它成爲遞歸形式
回到我們最初,即使使用遞歸,要做到的也是每排擺放一個皇后
so
void slove(int row) // 這裏改爲處理第row行皇后
{
// 這裏用來處理,第row行皇后分別放在col列上的結果
for( int col = 0; col < 8; ++col )
{
// 這裏進行遞歸處理
}
}
遞歸處理裏也是很簡單
// 這裏處理第row行第col列的皇后,是否可以擺放
void recursion(int row, int col)
{
// 判斷是否與之前的皇后衝突
if( !judge( row, col ) )
{
// 不發生衝突,將此位置記錄
arr[row] = col;
// 若達到第七行,表示一次擺放方法結束
if ( row == 7 ) {
disp(); // 輸出擺放結果
return;
}
// 繼續遍歷下一個皇后位置
slove( row + 1 );
}
// 發生衝突,不處理,退出此函數,會在slove函數中繼續處理後一列的擺放位置
}
最終slove方法稍加封裝可以這樣
void slove()
{
slove_( 0 ); // 處理第1行
}
void slove_(int row)
{
for( int col = 0; col < 8; ++col )
{
recursion( row, col );
}
}
void recursion(int row, int col)
{
if( !judge( row, col ) )
{
arr[row] = col;
if ( row == 7 ) {
disp();
return;
}
slove_( row + 1 );
}
}
當然,這個,也可以稍加封裝成類,就不再浪費大家時間了
最後,我關於回溯的理解,就是
循環+遞歸
好了,最後,拖一個以前寫的八皇后程序
這是基於Qt寫出來的demo程序,放出下載鏈,感興趣的可以看看