回溯法求解n皇后問題(複習)
回溯法
回溯法是最常用的解題方法,有“通用的解題法”之稱。當要解決的問題有若干可行解時,則可以在包含問題所有解的空間樹中,按深度優先的策略,從根節點出發搜尋解空間樹。演算法搜尋至解空間樹的任一結點時,總是先判斷該結點是否肯定不包含問題的解。如果肯定不包含,則跳過對以該結點為根的子樹的搜尋,繼續查詢該結點的兄弟結點,若它的兄弟結點都不包含問題的解,則返回其父結點——這個步驟稱為回溯。否則進入一個可能包含解的子樹,繼續按深度優先的策略進行搜尋。這種以深度優先的方式搜尋問題的解的演算法稱為回溯法。它本質上是一種窮舉法,但由於在搜尋過程中不斷略過某些顯然不合適的子樹,所以搜尋的空間大大少於一般的窮舉,故它適用於解一些組合數較大的問題。
總結一下:
一、基本定義
回溯法(back track method)是在包含問題的所有可能解的解空間樹中,從根結點出發,按照深度優先的策略進行搜尋,對於解空間樹的某個結點,若滿足約束條件,則進入該子樹繼續搜尋,否則將以該結點為根結點的子樹進行剪枝。
二、適用範圍
可避免搜尋所有的可能解,適用於求解組合數較大的問題。
三、n皇后問題
問題:在n x n的棋盤上擺放n個皇后,而且n個皇后中的任意兩個是不能處於同一行、同一列、或同一斜線上。
用陣列x[i](1≤i≤n)表示n後問題的解。其中x[i]表示皇后i放在棋盤的第i行的第x[i]列。由於不允許將2個皇后放在同一列,所以解向量中的x[i]互不相同。2個皇后不能放在同一斜線上是問題的隱約束。對於一般的n後問題,這一隱約束條件可以化成顯約束形式。設2個皇后放置位置為(i,j),(k,l):
顯然,棋盤的每一行上可以而且必須擺放一個皇后,所以,n皇后問題的可能解用一個n元向量X=(x1, x2, …, xn)表示,其中,1≤i≤n並且1≤xi≤n,即第i個皇后放在第i行第xi列上
由於兩個皇后不能位於同一列上,所以,解向量X必須滿足約束條件:
xi≠xj (式1)
若兩個皇后擺放的位置分別是(i, xi)和(j, xj),在棋盤上斜率為-1的斜線上,滿足條件i-j= xi-xj,在棋盤上斜率為1的斜線上,滿足條件i+j= xi+xj,綜合兩種情況,由於兩個皇后不能位於同一斜線
|i-xi|≠|j-xj| (式2)
為了簡化問題,下面討論四皇后問題:
四皇后問題的解空間樹是一個完全4叉樹,樹的根結點表示搜尋的初始狀態,對應Backtrack(1,x);從根結點到第2層結點對應皇后1在棋盤中第1行的可能擺放位置,從第2層結點到第3層結點對應皇后2在棋盤中第2行的可能擺放位置,依此類推。
完全4叉樹,我只畫了一部分,完整的應該是除了葉結點,每個內部結點都有四個子結點,k表示層數:
剪枝之後:
回溯法求解4皇后問題的搜尋過程:
當然這個圖只表示到找到的第一個解,我們知道還有另外一個解。
程式碼
變數sum記錄可行方案個數,初始為0;
n表示皇后個數,由使用者輸入;
x[]陣列儲存問題的解,表示皇后i放在棋盤的第i行第x[i]列,初始時各元素都為0,而我們目的是求出有多少組(x[1],x[2],x[3]……x[n])滿足擺放條件;
output(int x[])函式作用是輸出當前找到的一個可行解,只在搜尋到葉節點時才會呼叫;
Place(int k,int x[])函式作用是,對當前行k以上的所有行(即1到k-1行)逐行進行檢查,如果該行與上面任何一行相互攻擊(即位於同一對角線上了或同列了:abs(i-k)abs(x[i]-x[k]) || x[i]x[k]),那麼返回false,否則返回true;
Backtrack(int k,int x[])函式表示搜尋解空間中第k層子樹,k>n時,演算法搜尋至葉節點,得到一個新的n皇后互不攻擊放置方案,那麼輸出該方案,可行方案數sum加1;k<=n時,當前擴充套件節點是解空間的內部節點,該節點有x[1],x[2],x[3]……x[n]共n個子節點,對每一個子節點,用函式Place檢查其可行性,如果可行,以深度優先的方式遞迴地對可行子樹搜尋,如果不可行剪枝。
#include <iostream>
#include <cmath>
using namespace std;
// 記錄可行方案個數
int sum=0;
// 表示皇后個數,由使用者輸入
int n;
// 輸出當前找到的一個可行解,只在搜尋到葉節點時才會呼叫
int output(int x[]){
int i;
for(i=1;i<=n;i++){
cout << "(" << i << "," << x[i] << ")" << " ";
}
cout << endl;
return 0;
}
// 對當前行k以上的所有行(即1到k-1行)逐行進行檢查
bool Place(int k,int x[]){
int i;
for(i=1;i<k;i++){
if(abs(i-k)==abs(x[i]-x[k]) || x[i]==x[k])
return false;
}
return true;
}
// 見上文詳解
int Backtrack(int k,int x[]){
int i;
if(k>n){//如果是葉節點,直接輸出找到的一個解
output(x);
sum++;
}
else{//內部節點,如果滿足約束條件,繼續深度搜索 。i代表列數,從1到n
for(i=1;i<=n;i++){
x[k]=i;
if(Place(k,x))
Backtrack(k+1,x);
}
}
}
int main(){
int *x,i;
cout << "輸入皇后個數:" << endl;
cin >> n;
cout << endl;
// 陣列儲存問題的解,表示皇后i放在棋盤的第i行第x[i]列
x=new int[n+1];
// 初始時各元素都為0
for(i=0;i<=n;i++){
x[i]=0;
}
Backtrack(1,x);
cout << endl;
cout << "解的個數:" << sum << endl;
system("pause");
return 0;
}