1. 程式人生 > 其它 >回溯法求解n皇后問題(複習)

回溯法求解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,綜合兩種情況,由於兩個皇后不能位於同一斜線

上,所以,解向量X必須滿足約束條件:

|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;
}

執行結果