8皇后問題和由他推廣得到的N皇后問題來源於國際象棋的玩法,因爲皇后所在的位置可以縱向、橫向、兩個斜向四個方向的“捕捉”,所以8皇后問題就是要求如何佈置8個皇后在8*8的棋盤上而使他們互相無法“捕捉”。也就是說不存在兩個皇后同行或同列,或在同一斜線上。而N皇后問題就是如何佈置N個皇后在N*N棋盤裏使不存在兩個皇后在同行同列和同一斜線上。因爲8皇后問題可以歸爲N皇后問題,所以下面按照N皇后問題來進行討論。
解決N皇后問題的最好最著名的算法就是回溯法。在算法設計的基本方法中,回溯法是最一般的方法之一。在那些涉及到尋找一組解的問題或者求滿足某些約束條件的最優解的問題中,有許多可以用回溯法來求解。回溯法是一個既帶有系統性又帶有跳躍性的的搜索算法。它在包含問題的所有解的解空間樹中,按照深度優先的策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任一結點時,總是先判斷該結點是否肯定不包含問題的解。如果肯定不包含,則跳過對以該結點爲根的子樹的系統搜索,逐層向其祖先結點回溯。否則,進入該子樹,繼續按深度優先的策略進行搜索。回溯法在用來求問題的所有解時,要回溯到根,且根結點的所有子樹都已被搜索遍才結束。而回溯法在用來求問題的任一解時,只要搜索到問題的一個解就可以結束。這種以深度優先的方式系統地搜索問題的解的算法稱爲回溯法,它適用於解一些組合數較大的問題。
回溯法的基本思想:確定瞭解空間的組織結構後,回溯法就從開始結點(根結點)出發,以深度優先的方式搜索整個解空間。這個開始結點就成爲一個活結點,同時也成爲當前的擴展結點。在當前的擴展結點處,搜索向縱深方向移至一個新結點。這個新結點就成爲一個新的活結點,併成爲當前擴展結點。如果在當前的擴展結點處不能再向縱深方向移動,則當前擴展結點就成爲死結點。換句話說,這個結點不再是一個活結點。此時,應往回移動(回溯)至最近的一個活結點處,並使這個活結點成爲當前的擴展結點。回溯法即以這種工作方式遞歸地在解空間中搜索,直至找到所要求的解或解空間中已沒有活結點時爲止。
運用回溯法解題通常包含以下三個步驟:
(1)針對所給問題,定義問題的解空間;
(2)確定易於搜索的解空間結構;
(3)以深度優先的方式搜索解空間,並且在搜索過程中用剪枝函數避免無效搜索;
一般回溯法可用遞歸來實現,下面是從網上找來的一個非常典型的遞歸程序結構。
procedure try(i:integer);
var
begin
if i>n then 輸出結果
else for j:=下界 to 上界 do
begin
x[i]:=h[j];
if 可行{滿足限界函數和約束條件} then begin 置值;try(i+1); end;
end;
end;
說明:
i是遞歸深度;
n是深度控制,即解空間樹的的高度;
可行性判斷有兩方面的內容:不滿約束條件則剪去相應子樹;若限界函數越界,也剪去相應子樹;兩者均滿足則進入下一層,直到最後的葉子輸出結果。
回到N皇后問題的解決來,看看如何用回溯法解。首先找出解空間:給棋盤的行和列都編上1到N的號碼,皇后也給編上1到N的號碼。由於一個皇后應在不同的行上,爲不失一般性,可以假定第i個皇后將放在第i行上的某列。因此N皇后問題的解空間可以用一個N元組(X1,X2,.....Xn)來表示,其中Xi是放置皇后i所在的列號。這意味着所有的解都是N元組(1,2,3,.......,N)的置換。解空間大小爲N!。其次我們看約束條件:因爲解空間已經給我們排除了不在同一行(因爲每個皇后分別已經對應不同的行號)的約束條件。我們要判斷的是不在同一列和不在同一斜線的約束。因爲Xi表示皇后所在的列號,所以如果存在X(k)=X(i)那麼肯定存在第k個皇后和第i個皇后同列。所以不同列的判段條件是X(k)!=X(i),1<k<i 。又因爲同一斜線的特徵是要麼行號和列號之和不變(右高左低)要麼是行號和列號只差相等(左高右低),所以同斜線的判斷條件是 i+X(i)= k+X(k) 或 i-X(i) =k-X(k),兩式合併得 |X(i)-X(k)|=|i-k| 。
編程基本思路:X(j)表示一個解的空間,j表示行數,裏面的值表示可以放置在的列數,抽象約束條件得到能放置一個皇后的約束條件(1)X(i)!=X(k);(2)abs(X(i)-X(k))!=abs(i-k)。應用回溯法,當可以放置皇后時就繼續到下一行,不行的話就返回到第一行,重新檢驗要放的列數,如此反覆,直到將所有解解出。
#include <iostream.h>
#include <math.h>
/*檢查可不可以放置一個新的皇后*/
bool place(int k, int *X)
{
int i;
i=1;
while(i<k)
{
if((X[i]==X[k])||(abs(X[i]-X[k])==abs(i-k)))
return false;
i++;
}
return true;
}
/*求解問題的所有解*/
void Nqueens(int n,int *X)
{
int k;
X[1]=0;k=1;
while(k>0)
{
X[k]=X[k]+1; //不斷的在解空間裏從小到大的試探
while((X[k]<=n)&&(!place(k, X)))
X[k]=X[k]+1; //不符合條件的馬上再取解空間的下一個值來試探。
if(X[k]<=n) //找到了一個位置,而且是合法的
if(k==n) //是不是最後一個皇后,若是則得出一個完整解
{
for(int i=1;i<=n;i++)
cout<<X[i]<<" ";
cout<<"/n";
}
else //若不是最後一個皇后,則給下一個皇后找位置
{
k=k+1;
X[k]=0;
}
else k=k-1; //若是找了全部的列都無法放置某個皇后,則回溯到上一個k的情況,讓上一個k再往下試
}
}
/*主函數*/
void main()
{
cout<<"|--------------N皇后問題--------------|"<<endl;
cout<<"|-------------------------------------|"<<endl<<endl;
int n;
int *X;
int i;
while(i)
{
cout<<"請輸入皇后的個數:";
cin>>n;
X=new int[n];
cout<<"問題的解有:"<<endl;
Nqueens(n,X);
cout<<"Press<1> to run again"<<endl;
cout<<"Press<0> to exit"<<endl;
cin>>i;
}